home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 1865 / 1865.xpi / chrome / adblockplus.jar / content / ui / settings.js < prev    next >
Text File  |  2010-01-07  |  91KB  |  3,062 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Adblock Plus.
  15.  *
  16.  * The Initial Developer of the Original Code is
  17.  * Wladimir Palant.
  18.  * Portions created by the Initial Developer are Copyright (C) 2006-2009
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *
  23.  * ***** END LICENSE BLOCK ***** */
  24.  
  25. let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(Ci.nsIDragService);
  26.  
  27. const altMask = 2;
  28. const ctrlMask = 4;
  29. const metaMask = 8;
  30.  
  31. let accelMask = ctrlMask;
  32. try {
  33.     let prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
  34.     let accelKey = prefService.getIntPref("ui.key.accelKey");
  35.     if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META)
  36.         accelMask = metaMask;
  37.     else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT)
  38.         accelMask = altMask;
  39. } catch(e) {}
  40.  
  41. /**
  42.  * Location to be pre-set after initialization, as passed to setLocation().
  43.  * @type String
  44.  */
  45. let initWithLocation = null;
  46. /**
  47.  * Filter to be selected after initialization, as passed to selectFilter().
  48.  * @type Filter
  49.  */
  50. let initWithFilter = null;
  51.  
  52. /**
  53.  * Initialization function, called when the window is loaded.
  54.  */
  55. function init()
  56. {
  57.     // Insert Apply button between OK and Cancel
  58.     let okBtn = document.documentElement.getButton("accept");
  59.     let cancelBtn = document.documentElement.getButton("cancel");
  60.     let applyBtn = E("applyButton");
  61.     let insertBefore = cancelBtn;
  62.     for (let sibling = cancelBtn; sibling; sibling = sibling.nextSibling)
  63.         if (sibling == okBtn)
  64.             insertBefore = okBtn;
  65.     insertBefore.parentNode.insertBefore(applyBtn, insertBefore);
  66.     applyBtn.setAttribute("disabled", "true");
  67.     applyBtn.hidden = false;
  68.  
  69.     // Convert menubar into toolbar on Mac OS X
  70.     let isMac = (Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS == "Darwin");
  71.     if (isMac)
  72.     {
  73.         function copyAttributes(from, to)
  74.         {
  75.             for (let i = 0; i < from.attributes.length; i++)
  76.                 to.setAttribute(from.attributes[i].name, from.attributes[i].value);
  77.         }
  78.  
  79.         let menubar = E("menu");
  80.         let toolbar = document.createElement("toolbar");
  81.         copyAttributes(menubar, toolbar);
  82.  
  83.         for (let menu = menubar.firstChild; menu; menu = menu.nextSibling)
  84.         {
  85.             let button = document.createElement("toolbarbutton");
  86.             copyAttributes(menu, button);
  87.             button.setAttribute("type", "menu");
  88.             while (menu.firstChild)
  89.                 button.appendChild(menu.firstChild);
  90.             toolbar.appendChild(button);
  91.         }
  92.  
  93.         menubar.parentNode.replaceChild(toolbar, menubar);
  94.     }
  95.  
  96.     // Copy View menu contents into list header context menu
  97.     let viewMenu = E("view-popup").cloneNode(true);
  98.     let viewContext = E("treecols-context");
  99.     function replaceId(menuItem)
  100.     {
  101.         if (menuItem.id)
  102.             menuItem.id = "context-" + menuItem.id;
  103.         for (let child = menuItem.firstChild; child; child = child.nextSibling)
  104.             replaceId(child);
  105.     }
  106.     while (viewMenu.firstChild)
  107.     {
  108.         replaceId(viewMenu.firstChild);
  109.         viewContext.appendChild(viewMenu.firstChild);
  110.     }
  111.  
  112.     // Install listeners
  113.     filterStorage.addFilterObserver(onFilterChange);
  114.     filterStorage.addSubscriptionObserver(onSubscriptionChange);
  115.  
  116.     // Capture keypress events - need to get them before the tree does
  117.     E("listStack").addEventListener("keypress", onListKeyPress, true);
  118.  
  119.     // Use our fake browser with the findbar - and prevent default action on Enter key
  120.     E("findbar").browser = fastFindBrowser;
  121.     E("findbar").addEventListener("keypress", function(event)
  122.     {
  123.         // Work-around for bug 490047
  124.         if (event.keyCode == KeyEvent.DOM_VK_RETURN)
  125.             event.preventDefault();
  126.     }, false);
  127.     // Hack to prevent "highlight all" from getting enabled
  128.     E("findbar").toggleHighlight = function() {};
  129.  
  130.     // Initialize tree view
  131.     E("list").view = treeView;
  132.     treeView.setEditor(E("listEditor"), E("listEditorParent"));
  133.  
  134.     // Set the focus to the input field by default
  135.     E("list").focus();
  136.  
  137.     // Fire post-load handlers
  138.     let e = document.createEvent("Events");
  139.     e.initEvent("post-load", false, false);
  140.     window.dispatchEvent(e);
  141.  
  142.     // Execute these actions delayed to work around bug 489881
  143.     setTimeout(function()
  144.     {
  145.         if (initWithLocation)
  146.         {
  147.             treeView.editorDummyInit = initWithLocation;
  148.             treeView.selectRow(0);
  149.             if (!initWithFilter)
  150.                 treeView.startEditor(true);
  151.         }
  152.         if (initWithFilter)
  153.         {
  154.             treeView.selectFilter(getFilterByText(initWithFilter.text));
  155.             E("list").focus();
  156.         }
  157.         if (!initWithLocation && !initWithFilter)
  158.             treeView.ensureSelection(0);
  159.     }, 0);
  160. }
  161.  
  162. /**
  163.  * This should be called from "post-load" event handler to set the address that is
  164.  * supposed to be edited. This will initialize the editor and start the editor delayed
  165.  * (a subsequent call to selectFilter() will prevent the editor from opening).
  166.  * @param {String}  location  URL of the address to be taken as template of a new filter
  167.  */
  168. function setLocation(location)
  169. {
  170.     initWithLocation = location;
  171. }
  172.  
  173. /**
  174.  * This should be called from "post-load" event handler to select a particular filter
  175.  * in the list. If setLocation() was called before, this will also prevent the editor
  176.  * from opening (though it keeps editor's initial value in case the user opens the editor
  177.  * himself later).
  178.  * @param {Filter} filter  filter to be selected
  179.  */
  180. function selectFilter(filter)
  181. {
  182.     initWithFilter = filter;
  183. }
  184.  
  185. /**
  186.  * Cleanup function to remove observers, called when the window is unloaded.
  187.  */
  188. function cleanUp()
  189. {
  190.     filterStorage.removeFilterObserver(onFilterChange);
  191.     filterStorage.removeSubscriptionObserver(onSubscriptionChange);
  192. }
  193.  
  194. /**
  195.  * Map of all subscription wrappers by their download location.
  196.  * @type Object
  197.  */
  198. let subscriptionWrappers = {__proto__: null};
  199.  
  200. /**
  201.  * Creates a subscription wrapper that can be modified
  202.  * without affecting the original subscription. The properties
  203.  * _sortedFilters and _description are initialized immediately.
  204.  *
  205.  * @param {Subscription} subscription subscription to be wrapped
  206.  * @return {Subscription} subscription wrapper
  207.  */
  208. function createSubscriptionWrapper(subscription)
  209. {
  210.     if (subscription.url in subscriptionWrappers)
  211.         return subscriptionWrappers[subscription.url];
  212.  
  213.     let wrapper = 
  214.     {
  215.         __proto__: subscription,
  216.         _isWrapper: true,
  217.         _sortedFilters: subscription.filters,
  218.         _description: getSubscriptionDescription(subscription)
  219.     };
  220.     subscriptionWrappers[subscription.url] = wrapper;
  221.     return wrapper;
  222. }
  223.  
  224. /**
  225.  * Retrieves a subscription wrapper by the download location.
  226.  *
  227.  * @param {String} url download location of the subscription
  228.  * @return Subscription subscription wrapper or null for invalid URL
  229.  */
  230. function getSubscriptionByURL(url)
  231. {
  232.     if (url in subscriptionWrappers)
  233.     {
  234.         let result = subscriptionWrappers[url];
  235.         if (treeView.subscriptions.indexOf(result) < 0)
  236.             treeView.resortSubscription(result);
  237.         return result;
  238.     }
  239.     else
  240.     {
  241.         let result = abp.Subscription.fromURL(url);
  242.         if (!result || "_isWrapper" in result)
  243.             return result;
  244.  
  245.         result = createSubscriptionWrapper(result);
  246.         result.filters = result.filters.slice();
  247.         for (let i = 0; i < result.filters.length; i++)
  248.             result.filters[i] = getFilterByText(result.filters[i].text);
  249.  
  250.         treeView.resortSubscription(result);
  251.         return result;
  252.     }
  253. }
  254.  
  255. /**
  256.  * Map of all filter wrappers by their text representation.
  257.  * @type Object
  258.  */
  259. let filterWrappers = {__proto__: null};
  260.  
  261. /**
  262.  * Creates a filter wrapper that can be modified without affecting
  263.  * the original filter.
  264.  *
  265.  * @param {Filter} filter filter to be wrapped
  266.  * @return {Filter} filter wrapper
  267.  */
  268. function createFilterWrapper(filter)
  269. {
  270.     if (filter.text in filterWrappers)
  271.         return filterWrappers[filter.text];
  272.  
  273.     let wrapper = 
  274.     {
  275.         __proto__: filter,
  276.         _isWrapper: true
  277.     };
  278.     filterWrappers[filter.text] = wrapper;
  279.     return wrapper;
  280. }
  281.  
  282. /**
  283.  * Makes sure shortcut is initialized for the filter.
  284.  */
  285. function ensureFilterShortcut(/**Filter*/ filter)
  286. {
  287.     if (filter instanceof abp.RegExpFilter && !filter.shortcut)
  288.     {
  289.         let matcher = (filter instanceof abp.BlockingFilter ? abp.blacklistMatcher : abp.whitelistMatcher);
  290.         filter.shortcut = matcher.findShortcut(filter.text);
  291.     }
  292. }
  293.  
  294. /**
  295.  * Retrieves a filter by its text (might be a filter wrapper).
  296.  *
  297.  * @param {String} text text representation of the filter
  298.  * @return Filter
  299.  */
  300. function getFilterByText(text)
  301. {
  302.     if (text in filterWrappers)
  303.         return filterWrappers[text];
  304.     else
  305.     {
  306.         let result = abp.Filter.fromText(text);
  307.         ensureFilterShortcut(result);
  308.         return result;
  309.     }
  310. }
  311.  
  312. /**
  313.  * Generates the additional rows that should be shown as description
  314.  * of the subscription in the list.
  315.  *
  316.  * @param {Subscription} subscription
  317.  * @return {Array of String}
  318.  */
  319. function getSubscriptionDescription(subscription)
  320. {
  321.     let result = [];
  322.  
  323.     if (!(subscription instanceof abp.RegularSubscription))
  324.         return result;
  325.  
  326.     if (subscription instanceof abp.DownloadableSubscription && subscription.upgradeRequired)
  327.         result.push(abp.getString("subscription_wrong_version").replace(/--/, subscription.requiredVersion));
  328.  
  329.     if (subscription instanceof abp.DownloadableSubscription)
  330.         result.push(abp.getString("subscription_source") + " " + subscription.url);
  331.  
  332.     let status = "";
  333.     if (subscription instanceof abp.ExternalSubscription)
  334.         status += abp.getString("subscription_status_externaldownload");
  335.     else
  336.         status += (subscription.autoDownload ? abp.getString("subscription_status_autodownload") : abp.getString("subscription_status_manualdownload"));
  337.  
  338.     status += "; " + abp.getString("subscription_status_lastdownload") + " ";
  339.     if (synchronizer.isExecuting(subscription.url))
  340.         status += abp.getString("subscription_status_lastdownload_inprogress");
  341.     else
  342.     {
  343.         status += (subscription.lastDownload > 0 ? new Date(subscription.lastDownload * 1000).toLocaleFormat("%x %X") : abp.getString("subscription_status_lastdownload_unknown"));
  344.         if (subscription instanceof abp.DownloadableSubscription && subscription.downloadStatus)
  345.         {
  346.             try {
  347.                 status += " (" + abp.getString(subscription.downloadStatus) + ")";
  348.             } catch (e) {}
  349.         }
  350.     }
  351.  
  352.     result.push(abp.getString("subscription_status") + " " + status);
  353.     return result;
  354. }
  355.  
  356. /**
  357.  * Removes all filters from the list (after a warning).
  358.  */
  359. function clearList()
  360. {
  361.     if (confirm(abp.getString("clearall_warning")))
  362.         treeView.removeUserFilters();
  363. }
  364.  
  365. /**
  366.  * Shows a warning and resets hit statistics on the filters if the user confirms.
  367.  * @param {Boolean} resetAll  If true, statistics of all filters will be reset. If false, only selected filters will be reset.
  368.  */
  369. function resetHitCounts(resetAll)
  370. {
  371.     if (resetAll && confirm(abp.getString("resethitcounts_warning")))
  372.         filterStorage.resetHitCounts(null);
  373.     else if (!resetAll && confirm(abp.getString("resethitcounts_selected_warning")))
  374.     {
  375.         let filters = treeView.getSelectedFilters(false);
  376.         filterStorage.resetHitCounts(filters.map(function(filter)
  377.         {
  378.             return ("_isWrapper" in filter ? filter.__proto__ : filter);
  379.         }));
  380.     }
  381. }
  382.  
  383. /**
  384.  * Gets the default download dir, as used by the browser itself.
  385.  * @return {nsIFile}
  386.  * @see saveDefaultDir()
  387.  */
  388. function getDefaultDir()
  389. {
  390.     // Copied from Firefox: getTargetFile() in contentAreaUtils.js
  391.     try
  392.     {
  393.         return prefService.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
  394.     }
  395.     catch (e)
  396.     {
  397.         // No default download location. Default to desktop. 
  398.         let fileLocator = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
  399.     
  400.         return fileLocator.get("Desk", Ci.nsILocalFile);
  401.     }
  402. }
  403.  
  404. /**
  405.  * Saves new default download dir after the user chose a different directory to
  406.  * save his files to.
  407.  * @param {nsIFile} dir
  408.  * @see getDefaultDir()
  409.  */
  410. function saveDefaultDir(dir)
  411. {
  412.     // Copied from Firefox: getTargetFile() in contentAreaUtils.js
  413.     try
  414.     {
  415.         prefService.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, dir);
  416.     } catch(e) {};
  417. }
  418.  
  419. /**
  420.  * Adds a set of filters to the list.
  421.  * @param {Array of String} filters
  422.  * @return {Filter} last filter added (or null)
  423.  */
  424. function addFilters(filters)
  425. {
  426.     let commentQueue = [];
  427.     let lastAdded = null;
  428.     for each (let text in filters)
  429.     {
  430.         // Don't add checksum comments
  431.         if (/!\s*checksum[\s\-:]+([\w\+\/]+)/i.test(text))
  432.             continue;
  433.  
  434.         text = abp.normalizeFilter(text);
  435.         if (!text)
  436.             continue;
  437.  
  438.         let filter = getFilterByText(text);
  439.         if (filter instanceof abp.CommentFilter)
  440.             commentQueue.push(filter);
  441.         else
  442.         {
  443.             lastAdded = filter;
  444.             let subscription = treeView.addFilter(filter, null, null, true);
  445.             if (subscription && commentQueue.length)
  446.             {
  447.                 // Insert comments before the filter that follows them
  448.                 for each (let comment in commentQueue)
  449.                     treeView.addFilter(comment, subscription, filter, true);
  450.                 commentQueue.splice(0, commentQueue.length);
  451.             }
  452.         }
  453.     }
  454.  
  455.     for each (let comment in commentQueue)
  456.     {
  457.         lastAdded = comment;
  458.         treeView.addFilter(comment, null, null, true);
  459.     }
  460.  
  461.     return lastAdded;
  462. }
  463.  
  464. /**
  465.  * Lets the user choose a file and reads user-defined filters from this file.
  466.  */
  467. function importList()
  468. {
  469.     let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  470.     picker.init(window, abp.getString("import_filters_title"), picker.modeOpen);
  471.     picker.appendFilters(picker.filterText);
  472.     picker.appendFilters(picker.filterAll);
  473.  
  474.     let dir = getDefaultDir();
  475.     if (dir)
  476.         picker.displayDirectory = dir;
  477.  
  478.     if (picker.show() != picker.returnCancel)
  479.     {
  480.         saveDefaultDir(picker.file.parent.QueryInterface(Ci.nsILocalFile));
  481.         let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
  482.         fileStream.init(picker.file, 0x01, 0444, 0);
  483.  
  484.         let stream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
  485.         stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  486.         stream = stream.QueryInterface(Ci.nsIUnicharLineInputStream);
  487.  
  488.         let lines = [];
  489.         let line = {value: null};
  490.         while (stream.readLine(line))
  491.             lines.push(abp.normalizeFilter(line.value));
  492.         if (line.value)
  493.             lines.push(abp.normalizeFilter(line.value));
  494.         stream.close();
  495.  
  496.         if (/\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.test(lines[0]))
  497.         {
  498.             let minVersion = RegExp.$1;
  499.             let warning = "";
  500.             if (minVersion && abp.versionComparator.compare(minVersion, abp.getInstalledVersion()) > 0)
  501.                 warning = abp.getString("import_filters_wrong_version").replace(/--/, minVersion) + "\n\n";
  502.  
  503.             let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);
  504.             let flags = promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_0 +
  505.                                     promptService.BUTTON_TITLE_CANCEL * promptService.BUTTON_POS_1 +
  506.                                     promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_2;
  507.             let result = promptService.confirmEx(window, abp.getString("import_filters_title"),
  508.                 warning + abp.getString("import_filters_warning"), flags, abp.getString("overwrite"),
  509.                 null, abp.getString("append"), null, {});
  510.             if (result == 1)
  511.                 return;
  512.  
  513.             if (result == 0)
  514.                 treeView.removeUserFilters();
  515.  
  516.             lines.shift();
  517.             addFilters(lines);
  518.             treeView.ensureSelection(0);
  519.         }
  520.         else 
  521.             alert(abp.getString("invalid_filters_file"));
  522.     }
  523. }
  524.  
  525. /**
  526.  * Lets the user choose a file and writes user-defined filters into this file.
  527.  */
  528. function exportList()
  529. {
  530.     if (!treeView.hasUserFilters())
  531.         return;
  532.  
  533.     let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
  534.     picker.init(window, abp.getString("export_filters_title"), picker.modeSave);
  535.     picker.defaultExtension = ".txt";
  536.     picker.appendFilters(picker.filterText);
  537.     picker.appendFilters(picker.filterAll);
  538.  
  539.     let dir = getDefaultDir();
  540.     if (dir)
  541.         picker.displayDirectory = dir;
  542.  
  543.     if (picker.show() != picker.returnCancel)
  544.     {
  545.         saveDefaultDir(picker.file.parent.QueryInterface(Ci.nsILocalFile));
  546.         let lineBreak = abp.getLineBreak();
  547.  
  548.         let list = ["[Adblock]"];
  549.         let minVersion = "0";
  550.         for each (let subscription in treeView.subscriptions)
  551.         {
  552.             if (subscription instanceof abp.SpecialSubscription)
  553.             {
  554.                 for each (let filter in subscription.filters)
  555.                 {
  556.                     // Skip checksums
  557.                     if (filter instanceof abp.CommentFilter && /!\s*checksum[\s\-:]+([\w\+\/]+)/i.test(filter.text))
  558.                         continue;
  559.  
  560.                     list.push(filter.text);
  561.  
  562.                     // Find version requirements of this filter
  563.                     let filterVersion;
  564.                     if (filter instanceof abp.RegExpFilter)
  565.                     {
  566.                         if (/^(?:@@)?\|\|/.test(filter.text) || (!abp.Filter.regexpRegExp.test(filter.text) && /\^/.test(filter.text)))
  567.                             filterVersion = "1.1";
  568.                         else if (filter.includeDomains != null || filter.excludeDomains != null)
  569.                             filterVersion = "1.0.1";
  570.                         else if (filter.thirdParty != null)
  571.                             filterVersion = "1.0";
  572.                         else if (filter.collapse != null)
  573.                             filterVersion = "0.7.5";
  574.                         else if (abp.Filter.optionsRegExp.test(filter.text))
  575.                             filterVersion = "0.7.1";
  576.                         else if (/^(?:@@)?\|/.test(filter.text) || /\|$/.test(filter.text))
  577.                             filterVersion = "0.6.1.2";
  578.                         else
  579.                             filterVersion = "0";
  580.                     }
  581.                     else if (filter instanceof abp.ElemHideFilter)
  582.                     {
  583.                         if (filter.excludeDomains != null)
  584.                             filterVersion = "1.1";
  585.                         else if (/^#([\w\-]+|\*)(?:\(([\w\-]+)\))?$/.test(filter.text))
  586.                             filterVersion = "0.6.1";
  587.                         else
  588.                             filterVersion = "0.7";
  589.                     }
  590.                     else
  591.                         filterVersion = "0";
  592.                     
  593.                     // Adjust version requirements of the complete filter set
  594.                     if (filterVersion != "0" && abp.versionComparator.compare(minVersion, filterVersion) < 0)
  595.                         minVersion = filterVersion;
  596.                 }
  597.             }
  598.         }
  599.  
  600.         if (minVersion != "0")
  601.         {
  602.             if (abp.versionComparator.compare(minVersion, "0.7.1") >= 0)
  603.                 list[0] = "[Adblock Plus " + minVersion + "]";
  604.             else
  605.                 list[0] = "(Adblock Plus " + minVersion + " or higher required) " + list[0];
  606.         }
  607.  
  608.         list.push("");
  609.  
  610.         // Insert checksum
  611.         let checksum = abp.generateChecksum(list);
  612.         if (checksum)
  613.             list.splice(1, 0, "! Checksum: " + checksum);
  614.  
  615.         try
  616.         {
  617.             let fileStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
  618.             fileStream.init(picker.file, 0x02 | 0x08 | 0x20, 0644, 0);
  619.  
  620.             let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
  621.             stream.init(fileStream, "UTF-8", 16384, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
  622.  
  623.             stream.writeString(list.join(lineBreak));
  624.     
  625.             stream.close();
  626.         }
  627.         catch (e)
  628.         {
  629.             dump("Adblock Plus: error writing to file: " + e + "\n");
  630.             alert(abp.getString("filters_write_error"));
  631.         }
  632.     }
  633. }
  634.  
  635. /**
  636.  * Handles keypress event on the filter list
  637.  */
  638. function onListKeyPress(/**Event*/ e)
  639. {
  640.     // Ignore any keys directed to the editor
  641.     if (treeView.isEditing)
  642.         return;
  643.  
  644.     let modifiers = 0;
  645.     if (e.altKey)
  646.         modifiers |= altMask;
  647.     if (e.ctrlKey)
  648.         modifiers |= ctrlMask;
  649.     if (e.metaKey)
  650.         modifiers |= metaMask;
  651.  
  652.     if ((e.keyCode == e.DOM_VK_RETURN || e.keyCode == e.DOM_VK_ENTER) && modifiers)
  653.         document.documentElement.acceptDialog();
  654.     else if (e.keyCode == e.DOM_VK_RETURN || e.keyCode == e.DOM_VK_ENTER || e.keyCode == e.DOM_VK_F2)
  655.     {
  656.         e.preventDefault();
  657.         if (editFilter(null))
  658.             e.stopPropagation();
  659.     }
  660.     else if (e.keyCode == e.DOM_VK_DELETE || e.keyCode == e.DOM_VK_BACK_SPACE)
  661.         removeFilters(true);
  662.     else if (e.keyCode == e.DOM_VK_INSERT)
  663.         treeView.startEditor(true);
  664.     else if (e.charCode == e.DOM_VK_SPACE && !E("col-enabled").hidden)
  665.         toggleDisabled();
  666.     else if ((e.keyCode == e.DOM_VK_UP || e.keyCode == e.DOM_VK_DOWN) && modifiers == accelMask)
  667.     {
  668.         if (e.shiftKey)
  669.             treeView.moveSubscription(e.keyCode == e.DOM_VK_UP);
  670.         else
  671.             treeView.moveFilter(e.keyCode == e.DOM_VK_UP);
  672.         e.stopPropagation();
  673.     }
  674.     else if (String.fromCharCode(e.charCode).toLowerCase() == "t" && modifiers == accelMask)
  675.         synchSubscription(false);
  676. }
  677.  
  678. /**
  679.  * Handles click event on the filter list
  680.  */
  681. function onListClick(/**Event*/ e)
  682. {
  683.     if (e.button != 0)
  684.         return;
  685.  
  686.     let row = {};
  687.     let col = {};
  688.     treeView.boxObject.getCellAt(e.clientX, e.clientY, row, col, {});
  689.  
  690.     if (!col.value || col.value.id != "col-enabled")
  691.         return;
  692.  
  693.     let [subscription, filter] = treeView.getRowInfo(row.value);
  694.     if (subscription && !filter)
  695.         treeView.toggleDisabled([subscription]);
  696.     else if (filter instanceof abp.ActiveFilter)
  697.         treeView.toggleDisabled([filter]);
  698. }
  699.  
  700. /**
  701.  * Handles dblclick event on the filter list
  702.  */
  703. function onListDblClick(/**Event*/ e)
  704. {
  705.     if (e.button != 0)
  706.         return;
  707.  
  708.     let col = {};
  709.     treeView.boxObject.getCellAt(e.clientX, e.clientY, {}, col, {});
  710.  
  711.     if (col.value && col.value.id == "col-enabled")
  712.         return;
  713.  
  714.     editFilter(null);
  715. }
  716.  
  717. /**
  718.  * Handles draggesture event on the filter list, starts drag&drop session.
  719.  */
  720. function onListDragGesture(/**Event*/ e)
  721. {
  722.     treeView.startDrag(treeView.boxObject.getRowAt(e.clientX, e.clientY));
  723. }
  724.  
  725. /**
  726.  * Filter observer
  727.  * @see filterStorage.addFilterObserver()
  728.  */
  729. function onFilterChange(/**String*/ action, /**Array of Filter*/ filters, additionalData)
  730. {
  731.     switch (action)
  732.     {
  733.         case "add":
  734.             // addFilter() won't invalidate if the filter is already there because
  735.             // the subscription didn't create its subscription.filters copy yet,
  736.             // an update batch makes sure that everything is invalidated.
  737.             treeView.boxObject.beginUpdateBatch();
  738.             for each (let filter in filters)
  739.             {
  740.                 let insertBefore = (additionalData ? getFilterByText(additionalData.text) : null);
  741.                 treeView.addFilter(getFilterByText(filter.text), null, insertBefore, true);
  742.             }
  743.             treeView.boxObject.endUpdateBatch();
  744.             return;
  745.         case "remove":
  746.             // removeFilter() won't invalidate if the filter is already removed because
  747.             // the subscription didn't create its subscription.filters copy yet,
  748.             // an update batch makes sure that everything is invalidated.
  749.             treeView.boxObject.beginUpdateBatch();
  750.             for each (let filter in filters)
  751.                 treeView.removeFilter(null, getFilterByText(filter.text));
  752.             treeView.boxObject.endUpdateBatch();
  753.             return;
  754.         case "enable":
  755.         case "disable":
  756.             // Remove existing changes to "disabled" property
  757.             for each (let filter in filters)
  758.             {
  759.                 filter = getFilterByText(filter.text);
  760.                 if ("_isWrapper" in filter && filter.hasOwnProperty("disabled"))
  761.                     delete filter.disabled;
  762.             }
  763.             break;
  764.         case "hit":
  765.             if (E("col-hitcount").hidden && E("col-lasthit").hidden)
  766.             {
  767.                 // The data isn't visible, no need to invalidate
  768.                 return;
  769.             }
  770.             break;
  771.         default:
  772.             return;
  773.     }
  774.  
  775.     if (filters.length == 1)
  776.         treeView.invalidateFilter(getFilterByText(filters[0].text));
  777.     else
  778.         treeView.boxObject.invalidate();
  779. }
  780.  
  781. /**
  782.  * Subscription observer
  783.  * @see filterStorage.addSubscriptionObserver()
  784.  */
  785. function onSubscriptionChange(/**String*/ action, /**Array of Subscription*/ subscriptions)
  786. {
  787.     if (action == "reload")
  788.     {
  789.         // TODO: reinit?
  790.         return;
  791.     }
  792.  
  793.     for each (let subscription in subscriptions)
  794.     {
  795.         subscription = getSubscriptionByURL(subscription.url);
  796.         switch (action)
  797.         {
  798.             case "add":
  799.                 treeView.addSubscription(subscription, true);
  800.                 break;
  801.             case "remove":
  802.                 treeView.removeSubscription(subscription);
  803.                 break;
  804.             case "enable":
  805.             case "disable":
  806.                 // Remove existing changes to "disabled" property
  807.                 delete subscription.disabled;
  808.                 treeView.invalidateSubscription(subscription);
  809.                 break;
  810.             case "update":
  811.                 if ("oldSubscription" in subscription)
  812.                 {
  813.                     treeView.removeSubscription(getSubscriptionByURL(subscription.oldSubscription.url));
  814.                     delete subscriptionWrappers[subscription.oldSubscription.url];
  815.                     if (treeView.subscriptions.indexOf(subscription) < 0)
  816.                     {
  817.                         treeView.addSubscription(subscription, true);
  818.                         break;
  819.                     }
  820.                 }
  821.                 let oldCount = treeView.getSubscriptionRowCount(subscription);
  822.  
  823.                 delete subscription.filters;
  824.                 subscription.filters = subscription.filters.map(function(filter)
  825.                 {
  826.                     return getFilterByText(filter.text);
  827.                 });
  828.  
  829.                 treeView.resortSubscription(subscription);
  830.                 treeView.invalidateSubscription(subscription, oldCount);
  831.                 break;
  832.             case "updateinfo":
  833.                 if ("oldSubscription" in subscription)
  834.                 {
  835.                     treeView.removeSubscription(getSubscriptionByURL(subscription.oldSubscription.url));
  836.                     delete subscriptionWrappers[subscription.oldSubscription.url];
  837.                     if (treeView.subscriptions.indexOf(subscription) < 0)
  838.                     {
  839.                         treeView.addSubscription(subscription, true);
  840.                         break;
  841.                     }
  842.                 }
  843.                 treeView.invalidateSubscriptionInfo(subscription);
  844.                 break;
  845.         }
  846.     }
  847.  
  848.     // Date.toLocaleFormat() doesn't handle Unicode properly if called directly from XPCOM (bug 441370)
  849.     setTimeout(function()
  850.     {
  851.         for each (let subscription in subscriptions)
  852.         {
  853.             subscription = getSubscriptionByURL(subscription.url);
  854.             treeView.invalidateSubscriptionInfo(subscription);
  855.         }
  856.     }, 0);
  857. }
  858.  
  859. /**
  860.  * Starts editor for filter or subscription.
  861.  * @param {String} type  "filter", "subscription" or null (any)
  862.  */
  863. function editFilter(type) /**Boolean*/
  864. {
  865.     let [subscription, filter] = treeView.getRowInfo(treeView.selection.currentIndex);
  866.     if (!filter && !type)
  867.     {
  868.         // Don't do anything for group titles unless we were explicitly told what to do
  869.         return false;
  870.     }
  871.  
  872.     if (type != "filter" && subscription instanceof abp.RegularSubscription)
  873.         editSubscription(subscription);
  874.     else
  875.         treeView.startEditor(false);
  876.  
  877.     return true;
  878. }
  879.  
  880. /**
  881.  * Starts editor for a given subscription (pass null to add a new subscription).
  882.  */
  883. function editSubscription(/**Subscription*/ subscription)
  884. {
  885.     let result = {};
  886.     if (subscription)
  887.         openDialog("subscription.xul", "_blank", "chrome,centerscreen,modal", subscription, result);
  888.     else
  889.         openDialog("tip_subscriptions.xul", "_blank", "chrome,centerscreen,resizable,dialog=no,modal", result);
  890.  
  891.     if (!("url" in result))
  892.         return;
  893.  
  894.     let newSubscription = getSubscriptionByURL(result.url);
  895.     if (!newSubscription)
  896.         return;
  897.  
  898.     if (subscription && subscription != newSubscription)
  899.         treeView.removeSubscription(subscription);
  900.  
  901.     treeView.addSubscription(newSubscription);
  902.  
  903.     newSubscription.title = result.title;
  904.     newSubscription.disabled = result.disabled;
  905.     newSubscription.autoDownload = result.autoDownload;
  906.  
  907.     treeView.invalidateSubscriptionInfo(newSubscription);
  908.  
  909.     onChange();
  910.  
  911.     if (newSubscription instanceof abp.DownloadableSubscription && !newSubscription.lastDownload)
  912.         synchronizer.execute(newSubscription.__proto__);
  913. }
  914.  
  915. /**
  916.  * Removes the selected entries from the list and sets selection to the
  917.  * next item.
  918.  * @param {Boolean} allowSubscriptions  if true, a subscription will be
  919.  *                  removed if no removable filters are selected
  920.  */
  921. function removeFilters(allowSubscriptions)
  922. {
  923.     // Retrieve selected items
  924.     let selected = treeView.getSelectedInfo(false);
  925.  
  926.     let found = false;
  927.     for each (let [subscription, filter] in selected)
  928.     {
  929.         if (subscription instanceof abp.SpecialSubscription && filter instanceof abp.Filter)
  930.         {
  931.             treeView.removeFilter(subscription, filter);
  932.             found = true;
  933.         }
  934.     }
  935.  
  936.     if (found)
  937.         return;
  938.  
  939.     if (allowSubscriptions)
  940.     {
  941.         // No removable filters found, maybe we can remove a subscription?
  942.         let selectedSubscription = null;
  943.         for each (let [subscription, filter] in selected)
  944.         {
  945.             if (!selectedSubscription)
  946.                 selectedSubscription = subscription;
  947.             else if (selectedSubscription != subscription)
  948.                 return;
  949.         }
  950.  
  951.         if (selectedSubscription && selectedSubscription instanceof abp.RegularSubscription && confirm(abp.getString("remove_subscription_warning")))
  952.             treeView.removeSubscription(selectedSubscription);
  953.     }
  954. }
  955.  
  956. /**
  957.  * Enables or disables selected filters or the selected subscription
  958.  */
  959. function toggleDisabled()
  960. {
  961.     // Look for selected filters first
  962.     let selected = treeView.getSelectedFilters(true).filter(function(filter)
  963.     {
  964.         return filter instanceof abp.ActiveFilter;
  965.     });
  966.  
  967.     if (selected.length)
  968.         treeView.toggleDisabled(selected);
  969.     else
  970.     {
  971.         // No filters selected, maybe a subscription?
  972.         let [subscription, filter] = treeView.getRowInfo(treeView.selection.currentIndex);
  973.         if (subscription && !filter)
  974.             treeView.toggleDisabled([subscription]);
  975.     }
  976. }
  977.  
  978. /**
  979.  * Copies selected filters to clipboard.
  980.  */
  981. function copyToClipboard()
  982. {
  983.     let selected = treeView.getSelectedFilters(false);
  984.     if (!selected.length)
  985.         return;
  986.  
  987.     let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
  988.     let lineBreak = abp.getLineBreak();
  989.     clipboardHelper.copyString(selected.map(function(filter)
  990.     {
  991.         return filter.text;
  992.     }).join(lineBreak) + lineBreak);
  993. }
  994.  
  995. /**
  996.  * Pastes text as list of filters from clipboard
  997.  */
  998. function pasteFromClipboard() {
  999.     let clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
  1000.     let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
  1001.     transferable.addDataFlavor("text/unicode");
  1002.  
  1003.     try {
  1004.         clipboard.getData(transferable, clipboard.kGlobalClipboard);
  1005.     }
  1006.     catch (e) {
  1007.         return;
  1008.     }
  1009.  
  1010.     let data = {};
  1011.     transferable.getTransferData("text/unicode", data, {});
  1012.  
  1013.     try {
  1014.         data = data.value.QueryInterface(Ci.nsISupportsString).data;
  1015.     }
  1016.     catch (e) {
  1017.         return;
  1018.     }
  1019.  
  1020.     let lastAdded = addFilters(data.split(/[\r\n]+/));
  1021.     if (lastAdded)
  1022.         treeView.selectFilter(lastAdded);
  1023. }
  1024.  
  1025. /**
  1026.  * Starts synchronization of the currently selected subscription
  1027.  */
  1028. function synchSubscription(/**Boolean*/ forceDownload)
  1029. {
  1030.     let [subscription, filter] = treeView.getRowInfo(treeView.selection.currentIndex);
  1031.     if (subscription instanceof abp.DownloadableSubscription)
  1032.         synchronizer.execute(subscription.__proto__, forceDownload);
  1033. }
  1034.  
  1035. /**
  1036.  * Starts synchronization for all subscriptions
  1037.  */
  1038. function synchAllSubscriptions(/**Boolean*/ forceDownload)
  1039. {
  1040.     for each (let subscription in treeView.subscriptions)
  1041.         if (subscription instanceof abp.DownloadableSubscription)
  1042.             synchronizer.execute(subscription.__proto__, forceDownload);
  1043. }
  1044.  
  1045. /**
  1046.  * Updates the contents of the Filters menu, making sure the right
  1047.  * items are checked/enabled.
  1048.  */
  1049. function fillFiltersPopup()
  1050. {
  1051.     let empty = !treeView.hasUserFilters();
  1052.     E("export-command").setAttribute("disabled", empty);
  1053.     E("clearall").setAttribute("disabled", empty);
  1054. }
  1055.  
  1056. /**
  1057.  * Updates the contents of the View menu, making sure the right
  1058.  * items are checked/enabled.
  1059.  */
  1060. function fillViewPopup(/**String*/prefix)
  1061. {
  1062.     E(prefix + "view-filter").setAttribute("checked", !E("col-filter").hidden);
  1063.     E(prefix + "view-slow").setAttribute("checked", !E("col-slow").hidden);
  1064.     E(prefix + "view-enabled").setAttribute("checked", !E("col-enabled").hidden);
  1065.     E(prefix + "view-hitcount").setAttribute("checked", !E("col-hitcount").hidden);
  1066.     E(prefix + "view-lasthit").setAttribute("checked", !E("col-lasthit").hidden);
  1067.  
  1068.     let sortColumn = treeView.sortColumn;
  1069.     let sortColumnID = (sortColumn ? sortColumn.id : null);
  1070.     let sortDir = (sortColumn ? sortColumn.getAttribute("sortDirection") : "natural");
  1071.     E(prefix + "sort-none").setAttribute("checked", sortColumn == null);
  1072.     E(prefix + "sort-filter").setAttribute("checked", sortColumnID == "col-filter");
  1073.     E(prefix + "sort-enabled").setAttribute("checked", sortColumnID == "col-enabled");
  1074.     E(prefix + "sort-hitcount").setAttribute("checked", sortColumnID == "col-hitcount");
  1075.     E(prefix + "sort-lasthit").setAttribute("checked", sortColumnID == "col-lasthit");
  1076.     E(prefix + "sort-asc").setAttribute("checked", sortDir == "ascending");
  1077.     E(prefix + "sort-desc").setAttribute("checked", sortDir == "descending");
  1078. }
  1079.  
  1080. /**
  1081.  * Toggles visibility of a column.
  1082.  * @param {String} col  ID of the column to made visible/invisible
  1083.  */
  1084. function toggleColumn(col)
  1085. {
  1086.     col = E(col);
  1087.     col.setAttribute("hidden", col.hidden ? "false" : "true");
  1088. }
  1089.  
  1090. /**
  1091.  * Switches list sorting to the specified column. Sort order is kept.
  1092.  * @param {String} col  ID of the column to sort by or null for unsorted
  1093.  */
  1094. function sortBy(col)
  1095. {
  1096.     if (col)
  1097.         treeView.resort(E(col), treeView.sortColumn ? treeView.sortColumn.getAttribute("sortDirection") : "ascending");
  1098.     else
  1099.         treeView.resort(null, "natural");
  1100. }
  1101.  
  1102. /**
  1103.  * Changes sort order of the list. Sorts by filter column if the list is unsorted.
  1104.  * @param {String} order  either "ascending" or "descending"
  1105.  */
  1106. function setSortOrder(order)
  1107. {
  1108.     let col = treeView.sortColumn || E("col-filter");
  1109.     treeView.resort(col, order);
  1110. }
  1111.  
  1112. /**
  1113.  * Updates the contents of the Options menu, making sure the right
  1114.  * items are checked/enabled.
  1115.  */
  1116. function fillOptionsPopup()
  1117. {
  1118.     E("abp-enabled").setAttribute("checked", prefs.enabled);
  1119.     E("frameobjects").setAttribute("checked", prefs.frameobjects);
  1120.     E("slowcollapse").setAttribute("checked", !prefs.fastcollapse);
  1121.     E("showintoolbar").setAttribute("checked", prefs.showintoolbar);
  1122.     E("showinstatusbar").setAttribute("checked", prefs.showinstatusbar);
  1123. }
  1124.  
  1125. /**
  1126.  * Updates the contents of the context menu, making sure the right
  1127.  * items are checked/enabled.
  1128.  */
  1129. function fillContext()
  1130. {
  1131.     // Retrieve selected items
  1132.     let selected = treeView.getSelectedInfo(true);
  1133.  
  1134.     let currentSubscription = null;
  1135.     let currentFilter = null;
  1136.     if (selected.length)
  1137.         [currentSubscription, currentFilter] = selected[0];
  1138.  
  1139.     // Check whether all selected items belong to the same subscription
  1140.     let selectedSubscription = null;
  1141.     for each (let [subscription, filter] in selected)
  1142.     {
  1143.         if (!selectedSubscription)
  1144.             selectedSubscription = subscription;
  1145.         else if (subscription != selectedSubscription)
  1146.         {
  1147.             // More than one subscription selected, ignoring it
  1148.             selectedSubscription = null;
  1149.             break;
  1150.         }
  1151.     }
  1152.  
  1153.     // Check whether any patterns have been selected and whether any of them can be removed
  1154.     let hasFilters = selected.some(function(info)
  1155.     {
  1156.         let [subscription, filter] = info;
  1157.         return filter instanceof abp.Filter;
  1158.     });
  1159.     let hasRemovable = selected.some(function(info)
  1160.     {
  1161.         let [subscription, filter] = info;
  1162.         return subscription instanceof abp.SpecialSubscription && filter instanceof abp.Filter;
  1163.     });
  1164.     let activeFilters = selected.filter(function(info)
  1165.     {
  1166.         let [subscription, filter] = info;
  1167.         return filter instanceof abp.ActiveFilter;
  1168.     });
  1169.  
  1170.     if (selectedSubscription instanceof abp.RegularSubscription)
  1171.     {
  1172.         E("context-editsubscription").hidden = false;
  1173.         E("context-edit").hidden = true;
  1174.     }
  1175.     else
  1176.     {
  1177.         E("context-editsubscription").hidden = true;
  1178.         E("context-edit").hidden = false;
  1179.         E("context-edit").setAttribute("disabled", !(currentSubscription instanceof abp.SpecialSubscription && currentFilter instanceof abp.Filter));
  1180.     }
  1181.  
  1182.     E("context-synchsubscription").setAttribute("disabled", !(selectedSubscription instanceof abp.DownloadableSubscription));
  1183.     E("context-resethitcount").setAttribute("disabled", !hasFilters);
  1184.  
  1185.     E("context-moveup").setAttribute("disabled", !(currentSubscription instanceof abp.SpecialSubscription && currentFilter instanceof abp.Filter && !treeView.isSorted() && currentSubscription._sortedFilters.indexOf(currentFilter) > 0));
  1186.     E("context-movedown").setAttribute("disabled", !(currentSubscription instanceof abp.SpecialSubscription && currentFilter instanceof abp.Filter && !treeView.isSorted() && currentSubscription._sortedFilters.indexOf(currentFilter) < currentSubscription._sortedFilters.length - 1));
  1187.  
  1188.     E("context-movegroupup").setAttribute("disabled", !selectedSubscription || treeView.isFirstSubscription(selectedSubscription));
  1189.     E("context-movegroupdown").setAttribute("disabled", !selectedSubscription || treeView.isLastSubscription(selectedSubscription));
  1190.  
  1191.     let clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
  1192.  
  1193.     let hasFlavour = clipboard.hasDataMatchingFlavors(["text/unicode"], 1, clipboard.kGlobalClipboard);
  1194.  
  1195.     E("copy-command").setAttribute("disabled", !hasFilters);
  1196.     E("cut-command").setAttribute("disabled", !hasRemovable);
  1197.     E("paste-command").setAttribute("disabled", !hasFlavour);
  1198.     E("remove-command").setAttribute("disabled", !(hasRemovable || selectedSubscription instanceof abp.RegularSubscription));
  1199.  
  1200.     if (activeFilters.length || (selectedSubscription && !currentFilter))
  1201.     {
  1202.         let current = activeFilters.length ? activeFilters[0][1] : selectedSubscription;
  1203.         E("context-enable").hidden = !current.disabled;
  1204.         E("context-disable").hidden = current.disabled;
  1205.         E("context-disable").setAttribute("disabled", "false");
  1206.     }
  1207.     else
  1208.     {
  1209.         E("context-enable").hidden = true;
  1210.         E("context-disable").hidden = false;
  1211.         E("context-disable").setAttribute("disabled", "true");
  1212.     }
  1213.  
  1214.     return true;
  1215. }
  1216.  
  1217. /**
  1218.  * Toggles the value of a boolean preference.
  1219.  * @param {String} pref preference name (prefs object property)
  1220.  */
  1221. function togglePref(pref)
  1222. {
  1223.     prefs[pref] = !prefs[pref];
  1224.     prefs.save();
  1225. }
  1226.  
  1227. /**
  1228.  * Applies filter list changes.
  1229.  */
  1230. function applyChanges()
  1231. {
  1232.     treeView.applyChanges();
  1233.     E("applyButton").setAttribute("disabled", "true");
  1234. }
  1235.  
  1236. /**
  1237.  * Checks whether a tooltip should be shown and sets tooltip text appropriately
  1238.  */
  1239. function showTreeTooltip(/**Event*/ event) /**Boolean*/
  1240. {
  1241.     let col = {};
  1242.     let row = {};
  1243.     let childElement = {};
  1244.     treeView.boxObject.getCellAt(event.clientX, event.clientY, row, col, childElement);
  1245.  
  1246.     let [subscription, filter] = treeView.getRowInfo(row.value);
  1247.     if (row.value && col.value && col.value.id == "col-slow" && treeView.getCellText(row.value, col.value))
  1248.     {
  1249.         E("tree-tooltip").setAttribute("label", abp.getString("filter_regexp_tooltip"));
  1250.         return true;
  1251.     }
  1252.  
  1253.     if (filter instanceof abp.InvalidFilter && filter.reason)
  1254.     {
  1255.         E("tree-tooltip").setAttribute("label", filter.reason);
  1256.         return true;
  1257.     }
  1258.  
  1259.     if (row.value && col.value && treeView.boxObject.isCellCropped(row.value, col.value))
  1260.     {
  1261.         let text = treeView.getCellText(row.value, col.value);
  1262.         if (text)
  1263.         {
  1264.             E("tree-tooltip").setAttribute("label", text);
  1265.             return true;
  1266.         }
  1267.     }
  1268.  
  1269.     return false;
  1270. }
  1271.  
  1272. /**
  1273.  * Opens About Adblock Plus dialog
  1274.  */
  1275. function openAbout()
  1276. {
  1277.     openDialog("about.xul", "_blank", "chrome,centerscreen,modal");
  1278. }
  1279.  
  1280. /**
  1281.  * Should be called after each change to the filter list that needs applying later
  1282.  */
  1283. function onChange() {
  1284.     E("applyButton").removeAttribute("disabled");
  1285. }
  1286.  
  1287. /**
  1288.  * Sort function for the filter list, compares two filters by their text
  1289.  * representation.
  1290.  */
  1291. function compareText(/**Filter*/ filter1, /**Filter*/ filter2)
  1292. {
  1293.     if (filter1.text < filter2.text)
  1294.         return -1;
  1295.     else if (filter1.text > filter2.text)
  1296.         return 1;
  1297.     else
  1298.         return 0;
  1299. }
  1300.  
  1301. /**
  1302.  * Sort function for the filter list, compares two filters by "slow"
  1303.  * marker.
  1304.  */
  1305. function compareSlow(/**Filter*/ filter1, /**Filter*/ filter2)
  1306. {
  1307.     let isSlow1 = (filter1 instanceof abp.RegExpFilter && !filter1.disabled && !filter1.shortcut ? 1 : 0);
  1308.     let isSlow2 = (filter2 instanceof abp.RegExpFilter && !filter2.disabled && !filter2.shortcut ? 1 : 0);
  1309.     return isSlow1 - isSlow2;
  1310. }
  1311.  
  1312. /**
  1313.  * Sort function for the filter list, compares two filters by "enabled"
  1314.  * state.
  1315.  */
  1316. function compareEnabled(/**Filter*/ filter1, /**Filter*/ filter2)
  1317. {
  1318.     let hasEnabled1 = (filter1 instanceof abp.ActiveFilter ? 1 : 0);
  1319.     let hasEnabled2 = (filter2 instanceof abp.ActiveFilter ? 1 : 0);
  1320.     if (hasEnabled1 != hasEnabled2)
  1321.         return hasEnabled1 - hasEnabled2;
  1322.     else if (hasEnabled1 && filter1.disabled != filter2.disabled)
  1323.         return (filter1.disabled ? -1 : 1);
  1324.     else
  1325.         return 0;
  1326. }
  1327.  
  1328. /**
  1329.  * Sort function for the filter list, compares two filters by their hit count.
  1330.  */
  1331. function compareHitCount(/**Filter*/ filter1, /**Filter*/ filter2)
  1332. {
  1333.     let hasHitCount1 = (filter1 instanceof abp.ActiveFilter ? 1 : 0);
  1334.     let hasHitCount2 = (filter2 instanceof abp.ActiveFilter ? 1 : 0);
  1335.     if (hasHitCount1 != hasHitCount2)
  1336.         return hasHitCount1 - hasHitCount2;
  1337.     else if (hasHitCount1)
  1338.         return filter1.hitCount - filter2.hitCount;
  1339.     else
  1340.         return 0;
  1341. }
  1342.  
  1343. /**
  1344.  * Sort function for the filter list, compares two filters by their last hit.
  1345.  */
  1346. function compareLastHit(/**Filter*/ filter1, /**Filter*/ filter2)
  1347. {
  1348.     let hasLastHit1 = (filter1 instanceof abp.ActiveFilter ? 1 : 0);
  1349.     let hasLastHit2 = (filter2 instanceof abp.ActiveFilter ? 1 : 0);
  1350.     if (hasLastHit1 != hasLastHit2)
  1351.         return hasLastHit1 - hasLastHit2;
  1352.     else if (hasLastHit1)
  1353.         return filter1.lastHit - filter2.lastHit;
  1354.     else
  1355.         return 0;
  1356. }
  1357.  
  1358. /**
  1359.  * Creates a sort function from a primary and a secondary comparison function.
  1360.  * @param {Function} cmpFunc  comparison function to be called first
  1361.  * @param {Function} fallbackFunc  (optional) comparison function to be called if primary function returns 0
  1362.  * @param {Boolean} desc  if true, the result of the primary function (not the secondary function) will be reversed - sorting in descending order
  1363.  * @result {Function} comparison function to be used
  1364.  */
  1365. function createSortFunction(cmpFunc, fallbackFunc, desc)
  1366. {
  1367.     let factor = (desc ? -1 : 1);
  1368.  
  1369.     return function(filter1, filter2)
  1370.     {
  1371.         // Comment replacements without prototype always go last
  1372.         let isLast1 = (filter1.__proto__ == null);
  1373.         let isLast2 = (filter2.__proto__ == null);
  1374.         if (isLast1)
  1375.             return (isLast2 ? 0 : 1)
  1376.         else if (isLast2)
  1377.             return -1;
  1378.  
  1379.         let ret = cmpFunc(filter1, filter2);
  1380.         if (ret == 0 && fallbackFunc)
  1381.             return fallbackFunc(filter1, filter2);
  1382.         else
  1383.             return factor * ret;
  1384.     }
  1385. }
  1386.  
  1387. const nsITreeView = Ci.nsITreeView;
  1388.  
  1389. /**
  1390.  * nsITreeView implementation used for the filters list.
  1391.  * @class
  1392.  */
  1393. let treeView = {
  1394.     //
  1395.     // nsISupports implementation
  1396.     //
  1397.  
  1398.     QueryInterface: function(uuid) {
  1399.         if (!uuid.equals(Ci.nsISupports) &&
  1400.                 !uuid.equals(Ci.nsITreeView))
  1401.         {
  1402.             throw Cr.NS_ERROR_NO_INTERFACE;
  1403.         }
  1404.     
  1405.         return this;
  1406.     },
  1407.  
  1408.     //
  1409.     // nsITreeView implementation
  1410.     //
  1411.  
  1412.     setTree: function(boxObject)
  1413.     {
  1414.         if (!boxObject)
  1415.             return;
  1416.  
  1417.         this.boxObject = boxObject;
  1418.  
  1419.         let stringAtoms = ["col-filter", "col-enabled", "col-hitcount", "col-lasthit", "type-comment", "type-filterlist", "type-whitelist", "type-elemhide", "type-invalid"];
  1420.         let boolAtoms = ["selected", "dummy", "subscription", "description", "filter", "filter-regexp", "subscription-special", "subscription-external", "subscription-autoDownload", "subscription-disabled", "subscription-upgradeRequired", "subscription-dummy", "filter-disabled"];
  1421.         let atomService = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
  1422.  
  1423.         this.atoms = {};
  1424.         for each (let atom in stringAtoms)
  1425.             this.atoms[atom] = atomService.getAtom(atom);
  1426.         for each (let atom in boolAtoms)
  1427.         {
  1428.             this.atoms[atom + "-true"] = atomService.getAtom(atom + "-true");
  1429.             this.atoms[atom + "-false"] = atomService.getAtom(atom + "-false");
  1430.         }
  1431.  
  1432.         // Copy the subscription list, we don't want to apply our changes immediately
  1433.         this.subscriptions = filterStorage.subscriptions.map(createSubscriptionWrapper);
  1434.  
  1435.         this.closed = {__proto__: null};
  1436.         let closed = this.boxObject.treeBody.parentNode.getAttribute("closedSubscriptions");
  1437.         if (closed)
  1438.             for each (let id in closed.split(" "))
  1439.                 this.closed[id] = true;
  1440.  
  1441.         // Check current sort direction
  1442.         let cols = document.getElementsByTagName("treecol");
  1443.         let sortColumn = null;
  1444.         let sortDir = null;
  1445.         for (let i = 0; i < cols.length; i++)
  1446.         {
  1447.             let col = cols[i];
  1448.             let dir = col.getAttribute("sortDirection");
  1449.             if (dir && dir != "natural")
  1450.             {
  1451.                 sortColumn = col;
  1452.                 sortDir = dir;
  1453.             }
  1454.         }
  1455.  
  1456.         if (sortColumn)
  1457.             this.resort(sortColumn, sortDir);
  1458.  
  1459.         // Make sure we stop the editor when scrolling
  1460.         let me = this;
  1461.         this.boxObject.treeBody.addEventListener("DOMMouseScroll", function()
  1462.         {
  1463.             me.stopEditor(true);
  1464.         }, false);
  1465.     },
  1466.  
  1467.     get rowCount()
  1468.     {
  1469.         let count = 0;
  1470.         for each (let subscription in this.subscriptions)
  1471.         {
  1472.             // Special subscriptions are only shown if they aren't empty
  1473.             if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 0)
  1474.                 continue;
  1475.  
  1476.             count++;
  1477.             if (!(subscription.url in this.closed))
  1478.                 count += subscription._description.length + subscription._sortedFilters.length;
  1479.         }
  1480.  
  1481.         return count;
  1482.     },
  1483.  
  1484.     getCellText: function(row, col)
  1485.     {
  1486.         col = col.id;
  1487.  
  1488.         // Only three columns have text
  1489.         if (col != "col-filter" && col != "col-slow" && col != "col-hitcount" && col != "col-lasthit")
  1490.             return null;
  1491.  
  1492.         // Don't show text in the edited row
  1493.         if (col == "col-filter" && this.editedRow == row)
  1494.             return null;
  1495.  
  1496.         let [subscription, filter] = this.getRowInfo(row);
  1497.         if (!subscription)
  1498.             return null;
  1499.  
  1500.         if (filter instanceof abp.Filter)
  1501.         {
  1502.             if (col == "col-filter")
  1503.                 return filter.text;
  1504.             else if (col == "col-slow")
  1505.                 return (filter instanceof abp.RegExpFilter && !filter.shortcut && !filter.disabled && !subscription.disabled ? "!" : null);
  1506.             else if (filter instanceof abp.ActiveFilter)
  1507.             {
  1508.                 if (col == "col-hitcount")
  1509.                     return filter.hitCount;
  1510.                 else
  1511.                     return (filter.lastHit ? new Date(filter.lastHit).toLocaleFormat("%x %X") : null);
  1512.             }
  1513.             else
  1514.                 return null;
  1515.         }
  1516.         else if (col != "col-filter")
  1517.             return null;
  1518.         else if (!filter)
  1519.             return (subscription instanceof abp.RegularSubscription ? this.titlePrefix : "") + subscription.title;
  1520.         else
  1521.             return filter;
  1522.     },
  1523.  
  1524.     getColumnProperties: function(col, properties)
  1525.     {
  1526.         col = col.id;
  1527.  
  1528.         if (col in this.atoms)
  1529.             properties.AppendElement(this.atoms[col]);
  1530.     },
  1531.  
  1532.     getRowProperties: function(row, properties)
  1533.     {
  1534.         let [subscription, filter] = this.getRowInfo(row);
  1535.         if (!subscription)
  1536.             return;
  1537.  
  1538.         properties.AppendElement(this.atoms["selected-" + this.selection.isSelected(row)]);
  1539.         properties.AppendElement(this.atoms["subscription-" + !filter]);
  1540.         properties.AppendElement(this.atoms["filter-" + (filter instanceof abp.Filter)]);
  1541.         properties.AppendElement(this.atoms["filter-regexp-" + (filter instanceof abp.RegExpFilter && !filter.shortcut)]);
  1542.         properties.AppendElement(this.atoms["description-" + (typeof filter == "string")]);
  1543.         properties.AppendElement(this.atoms["subscription-special-" + (subscription instanceof abp.SpecialSubscription)]);
  1544.         properties.AppendElement(this.atoms["subscription-external-" + (subscription instanceof abp.ExternalSubscription)]);
  1545.         properties.AppendElement(this.atoms["subscription-autoDownload-" + (subscription instanceof abp.DownloadableSubscription && subscription.autoDownload)]);
  1546.         properties.AppendElement(this.atoms["subscription-disabled-" + subscription.disabled]);
  1547.         properties.AppendElement(this.atoms["subscription-upgradeRequired-" + (subscription instanceof abp.DownloadableSubscription && subscription.upgradeRequired)]);
  1548.         properties.AppendElement(this.atoms["subscription-dummy-" + (subscription instanceof abp.Subscription && subscription.url == "~dummy~")]);
  1549.         if (filter instanceof abp.Filter)
  1550.         {
  1551.             if (filter instanceof abp.ActiveFilter)
  1552.                 properties.AppendElement(this.atoms["filter-disabled-" + filter.disabled]);
  1553.  
  1554.             if (filter instanceof abp.CommentFilter)
  1555.                 properties.AppendElement(this.atoms["type-comment"]);
  1556.             else if (filter instanceof abp.BlockingFilter)
  1557.                 properties.AppendElement(this.atoms["type-filterlist"]);
  1558.             else if (filter instanceof abp.WhitelistFilter)
  1559.                 properties.AppendElement(this.atoms["type-whitelist"]);
  1560.             else if (filter instanceof abp.ElemHideFilter)
  1561.                 properties.AppendElement(this.atoms["type-elemhide"]);
  1562.             else if (filter instanceof abp.InvalidFilter)
  1563.                 properties.AppendElement(this.atoms["type-invalid"]);
  1564.         }
  1565.     },
  1566.  
  1567.     getCellProperties: function(row, col, properties)
  1568.     {
  1569.         this.getColumnProperties(col, properties);
  1570.         this.getRowProperties(row, properties);
  1571.     },
  1572.  
  1573.     isContainer: function(row)
  1574.     {
  1575.         let [subscription, filter] = this.getRowInfo(row);
  1576.         return subscription && !filter;
  1577.     },
  1578.  
  1579.     isContainerOpen: function(row)
  1580.     {
  1581.         let [subscription, filter] = this.getRowInfo(row);
  1582.         return subscription && !filter && !(subscription.url in this.closed);
  1583.     },
  1584.  
  1585.     isContainerEmpty: function(row)
  1586.     {
  1587.         let [subscription, filter] = this.getRowInfo(row);
  1588.         return subscription && !filter && subscription._description.length + subscription._sortedFilters.length == 0;
  1589.     },
  1590.  
  1591.     getLevel: function(row)
  1592.     {
  1593.         let [subscription, filter] = this.getRowInfo(row);
  1594.         return (filter ? 1 : 0);
  1595.     },
  1596.  
  1597.     getParentIndex: function(row)
  1598.     {
  1599.         let [subscription, filter] = this.getRowInfo(row);
  1600.         return (subscription && filter ? this.getSubscriptionRow(subscription) : -1);
  1601.     },
  1602.  
  1603.     hasNextSibling: function(row, afterRow)
  1604.     {
  1605.         let [subscription, filter] = this.getRowInfo(row);
  1606.         if (!filter)
  1607.             return false;
  1608.  
  1609.         let startIndex = this.getSubscriptionRow(subscription);
  1610.         if (startIndex < 0)
  1611.             return false;
  1612.  
  1613.         return (startIndex + subscription._description.length + subscription._sortedFilters.length > afterRow);
  1614.     },
  1615.  
  1616.     toggleOpenState: function(row)
  1617.     {
  1618.         let [subscription, filter] = this.getRowInfo(row);
  1619.         if (!subscription || filter)
  1620.             return;
  1621.  
  1622.         let count = subscription._description.length + subscription._sortedFilters.length;
  1623.         if (subscription.url in this.closed)
  1624.         {
  1625.             delete this.closed[subscription.url];
  1626.             this.boxObject.rowCountChanged(row + 1, count);
  1627.         }
  1628.         else
  1629.         {
  1630.             this.closed[subscription.url] = true;
  1631.             this.boxObject.rowCountChanged(row + 1, -count);
  1632.         }
  1633.         this.boxObject.invalidateRow(row);
  1634.  
  1635.         // Update closedSubscriptions attribute so that the state persists
  1636.         let closed = [];
  1637.         for (let url in this.closed)
  1638.             closed.push(url);
  1639.         this.boxObject.treeBody.parentNode.setAttribute("closedSubscriptions", closed.join(" "));
  1640.     },
  1641.  
  1642.     cycleHeader: function(col)
  1643.     {
  1644.         col = col.element;
  1645.  
  1646.         let cycle =
  1647.         {
  1648.             natural: 'ascending',
  1649.             ascending: 'descending',
  1650.             descending: 'natural'
  1651.         };
  1652.  
  1653.         let curDirection = "natural";
  1654.         if (this.sortColumn == col)
  1655.             curDirection = col.getAttribute("sortDirection");
  1656.         else if (this.sortColumn)
  1657.             this.sortColumn.removeAttribute("sortDirection");
  1658.  
  1659.         this.resort(col, cycle[curDirection]);
  1660.     },
  1661.  
  1662.     isSorted: function()
  1663.     {
  1664.         return (this.sortProc != null);
  1665.     },
  1666.  
  1667.     canDrop: function(row, orientation)
  1668.     {
  1669.         let session = dragService.getCurrentSession();
  1670.         if (!session || session.sourceNode != this.boxObject.treeBody || !this.dragSubscription || orientation == nsITreeView.DROP_ON)
  1671.             return false;
  1672.  
  1673.         let [subscription, filter] = this.getRowInfo(row);
  1674.         if (!subscription)
  1675.             return false;
  1676.  
  1677.         if (this.dragFilter)
  1678.         {
  1679.             // Dragging a filter
  1680.             return filter && subscription instanceof abp.SpecialSubscription && subscription.isFilterAllowed(this.dragFilter);
  1681.         }
  1682.         else
  1683.         {
  1684.             // Dragging a subscription
  1685.             return true;
  1686.         }
  1687.     },
  1688.  
  1689.     drop: function(row, orientation)
  1690.     {
  1691.         let session = dragService.getCurrentSession();
  1692.         if (!session || session.sourceNode != this.boxObject.treeBody || !this.dragSubscription || orientation == nsITreeView.DROP_ON)
  1693.             return;
  1694.  
  1695.         let [subscription, filter] = this.getRowInfo(row);
  1696.         if (!subscription)
  1697.             return;
  1698.  
  1699.         if (this.dragFilter)
  1700.         {
  1701.             // Dragging a filter
  1702.             if (!(filter && subscription instanceof abp.SpecialSubscription && subscription.isFilterAllowed(this.dragFilter)))
  1703.                 return;
  1704.  
  1705.             let oldSubscription = this.dragSubscription;
  1706.             let oldSortedIndex = oldSubscription._sortedFilters.indexOf(this.dragFilter);
  1707.             let newSortedIndex = subscription._sortedFilters.indexOf(filter);
  1708.             if (oldSortedIndex < 0 || newSortedIndex < 0)
  1709.                 return;
  1710.             if (orientation == nsITreeView.DROP_AFTER)
  1711.                 newSortedIndex++;
  1712.  
  1713.             let oldIndex = (oldSubscription.filters == oldSubscription._sortedFilters ? oldSortedIndex : oldSubscription.filters.indexOf(this.dragFilter));
  1714.             let newIndex = (subscription.filters == subscription._sortedFilters || newSortedIndex >= subscription._sortedFilters.length ? newSortedIndex : subscription.filters.indexOf(subscription._sortedFilters[newSortedIndex]));
  1715.             if (oldIndex < 0 || newIndex < 0)
  1716.                 return;
  1717.             if (oldSubscription == subscription && (newIndex == oldIndex || newIndex == oldIndex + 1))
  1718.                 return;
  1719.  
  1720.             {
  1721.                 if (!oldSubscription.hasOwnProperty("filters"))
  1722.                     oldSubscription.filters = oldSubscription.filters.slice();
  1723.  
  1724.                 let rowCountBefore = treeView.getSubscriptionRowCount(oldSubscription);
  1725.                 let row = treeView.getSubscriptionRow(oldSubscription) + rowCountBefore - oldSubscription._sortedFilters.length + oldSortedIndex;
  1726.                 oldSubscription.filters.splice(oldIndex, 1);
  1727.                 this.resortSubscription(oldSubscription);
  1728.                 let rowCountAfter = treeView.getSubscriptionRowCount(oldSubscription);
  1729.                 this.boxObject.rowCountChanged(row + 1 + rowCountAfter - rowCountBefore, rowCountAfter - rowCountBefore);
  1730.             }
  1731.  
  1732.             if (oldSubscription == subscription && newSortedIndex > oldSortedIndex)
  1733.                 newSortedIndex--;
  1734.             if (oldSubscription == subscription && newIndex > oldIndex)
  1735.                 newIndex--;
  1736.  
  1737.             {
  1738.                 if (!subscription.hasOwnProperty("filters"))
  1739.                     subscription.filters = subscription.filters.slice();
  1740.  
  1741.                 let rowCountBefore = treeView.getSubscriptionRowCount(subscription);
  1742.                 subscription.filters.splice(newIndex, 0, this.dragFilter);
  1743.                 this.resortSubscription(subscription);
  1744.                 let rowCountAfter = treeView.getSubscriptionRowCount(subscription);
  1745.                 let row = treeView.getSubscriptionRow(subscription) + rowCountAfter - subscription._sortedFilters.length + newSortedIndex;
  1746.                 this.boxObject.rowCountChanged(row + 1 + rowCountBefore - rowCountAfter, rowCountAfter - rowCountBefore);
  1747.  
  1748.                 treeView.selectRow(row);
  1749.             }
  1750.         }
  1751.         else
  1752.         {
  1753.             // Dragging a subscription
  1754.             if (subscription == this.dragSubscription)
  1755.                 return;
  1756.  
  1757.             let rowCount = this.getSubscriptionRowCount(this.dragSubscription);
  1758.  
  1759.             let oldIndex = this.subscriptions.indexOf(this.dragSubscription);
  1760.             let newIndex = this.subscriptions.indexOf(subscription);
  1761.             if (oldIndex < 0 || newIndex < 0)
  1762.                 return;
  1763.  
  1764.             if (filter && oldIndex > newIndex)
  1765.                 orientation = nsITreeView.DROP_BEFORE;
  1766.             else if (filter)
  1767.                 orientation = nsITreeView.DROP_AFTER;
  1768.  
  1769.             let oldRow = this.getSubscriptionRow(this.dragSubscription);
  1770.             this.subscriptions.splice(oldIndex, 1);
  1771.             this.boxObject.rowCountChanged(oldRow, -rowCount);
  1772.  
  1773.             if (orientation == nsITreeView.DROP_AFTER)
  1774.                 newIndex++;
  1775.             if (oldIndex < newIndex)
  1776.                 newIndex--;
  1777.  
  1778.             this.subscriptions.splice(newIndex, 0, this.dragSubscription);
  1779.             let newRow = this.getSubscriptionRow(this.dragSubscription);
  1780.             this.boxObject.rowCountChanged(newRow, rowCount);
  1781.  
  1782.             treeView.selectRow(newRow);
  1783.         }
  1784.  
  1785.         onChange();
  1786.     },
  1787.  
  1788.     getCellValue: function() {return null},
  1789.     getProgressMode: function() {return null},
  1790.     getImageSrc: function() {return null},
  1791.     isSeparator: function() {return false},
  1792.     isEditable: function() {return false},
  1793.     cycleCell: function() {},
  1794.     performAction: function() {},
  1795.     performActionOnRow: function() {},
  1796.     performActionOnCell: function() {},
  1797.     selection: null,
  1798.     selectionChanged: function() {},
  1799.  
  1800.     //
  1801.     // Custom properties and methods
  1802.     //
  1803.  
  1804.     /**
  1805.      * List of subscriptions displayed
  1806.      * @type Array of Subscription
  1807.      */
  1808.     subscriptions: null,
  1809.  
  1810.     /**
  1811.      * Box object of the tree
  1812.      * @type nsITreeBoxObject
  1813.      */
  1814.     boxObject: null,
  1815.  
  1816.     /**
  1817.      * Map containing URLs of subscriptions that are displayed collapsed
  1818.      * @type Object
  1819.      */
  1820.     closed: null,
  1821.  
  1822.     /**
  1823.      * String to be displayed before the title of regular subscriptions
  1824.      * @type String
  1825.      * @const
  1826.      */
  1827.     titlePrefix: abp.getString("subscription_description") + " ",
  1828.  
  1829.     /**
  1830.      * Map of atoms being used as col/row/cell properties, String => nsIAtom
  1831.      * @type Object
  1832.      */
  1833.     atoms: null,
  1834.  
  1835.     /**
  1836.      * Column by which the list is sorted or null for natural order
  1837.      * @type Element
  1838.      */
  1839.     sortColumn: null,
  1840.  
  1841.     /**
  1842.      * Comparison function used to sort the list or null for natural order
  1843.      * @type Function
  1844.      */
  1845.     sortProc: null,
  1846.  
  1847.     /**
  1848.      * Returns the first row of a subscription in the list or -1 if the
  1849.      * subscription isn't in the list or isn't visible.
  1850.      */
  1851.     getSubscriptionRow: function(/**Subscription*/ search)  /**Integer*/
  1852.     {
  1853.         let index = 0;
  1854.         for each (let subscription in this.subscriptions)
  1855.         {
  1856.             let rowCount = this.getSubscriptionRowCount(subscription);
  1857.             if (rowCount > 0 && search == subscription)
  1858.                 return index;
  1859.  
  1860.             index += rowCount;
  1861.         }
  1862.         return -1;
  1863.     },
  1864.  
  1865.     /**
  1866.      * Returns the number of rows used to display the subscription in the list.
  1867.      */
  1868.     getSubscriptionRowCount: function(/**Subscription*/ subscription) /**Integer*/
  1869.     {
  1870.         if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 0)
  1871.             return 0;
  1872.  
  1873.         if (subscription.url in this.closed)
  1874.             return 1;
  1875.  
  1876.         return 1 + subscription._description.length + subscription._sortedFilters.length;
  1877.     },
  1878.  
  1879.     /**
  1880.      * Returns the filter displayed in the given row and the corresponding filter subscription.
  1881.      * @param {Integer} row   row index
  1882.      * @return {Array}  array with two elements indicating the contents of the row:
  1883.      *                    [null, null] - empty row
  1884.      *                    [Subscription, null] - subscription title row
  1885.      *                    [Subscription, String] - subscription description row (row text is second array element)
  1886.      *                    [Subscription, Filter] - filter from the given subscription
  1887.      */
  1888.     getRowInfo: function(row)
  1889.     {
  1890.         for each (let subscription in this.subscriptions)
  1891.         {
  1892.             // Special subscriptions are only shown if they aren't empty
  1893.             if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 0)
  1894.                 continue;
  1895.  
  1896.             // Check whether the subscription row has been requested
  1897.             row--;
  1898.             if (row < 0)
  1899.                 return [subscription, null];
  1900.  
  1901.             if (!(subscription.url in this.closed))
  1902.             {
  1903.                 // Check whether the subscription description row has been requested
  1904.                 if (row < subscription._description.length)
  1905.                     return [subscription, subscription._description[row]];
  1906.  
  1907.                 row -= subscription._description.length;
  1908.  
  1909.                 // Check whether one of the filters has been requested
  1910.                 if (row < subscription._sortedFilters.length)
  1911.                     return [subscription, subscription._sortedFilters[row]];
  1912.  
  1913.                 row -= subscription._sortedFilters.length;
  1914.             }
  1915.         }
  1916.  
  1917.         return [null, null];
  1918.     },
  1919.  
  1920.     /**
  1921.      * Returns the filters currently selected.
  1922.      * @param {Boolean} prependCurrent if true, current element will be returned first
  1923.      * @return {Array of Filter}
  1924.      */
  1925.     getSelectedFilters: function(prependCurrent)
  1926.     {
  1927.         return this.getSelectedInfo(prependCurrent).map(function(info)
  1928.         {
  1929.             return info[1];
  1930.         }).filter(function(filter)
  1931.         {
  1932.             return filter instanceof abp.Filter;
  1933.         });
  1934.     },
  1935.  
  1936.     /**
  1937.      * Returns the filters/subscription currently selected.
  1938.      * @param {Boolean} prependCurrent if true, current element will be returned first
  1939.      * @return {Array} each array entry has the same format as treeView.getRowInfo() result
  1940.      * @see treeView.getRowInfo()
  1941.      */
  1942.     getSelectedInfo: function(prependCurrent)
  1943.     {
  1944.         let result = [];
  1945.         for (let i = 0; i < this.selection.getRangeCount(); i++)
  1946.         {
  1947.             let min = {};
  1948.             let max = {};
  1949.             this.selection.getRangeAt(i, min, max);
  1950.             for (let j = min.value; j <= max.value; j++)
  1951.             {
  1952.                 let info = this.getRowInfo(j);
  1953.                 if (info[0])
  1954.                 {
  1955.                     if (prependCurrent && j == treeView.selection.currentIndex)
  1956.                         result.unshift(info);
  1957.                     else
  1958.                         result.push(info);
  1959.                 }
  1960.             }
  1961.         }
  1962.         return result;
  1963.     },
  1964.  
  1965.     /**
  1966.      * Checks whether the filter already has a wrapper. If
  1967.      * not, replaces all instances of the filter but the
  1968.      * wrapper.
  1969.      * @param {Filter} filter   filter to be tested
  1970.      * @return {Filter} wrapped filter
  1971.      */
  1972.     ensureFilterWrapper: function(filter)
  1973.     {
  1974.         if ("_isWrapper" in filter)
  1975.             return filter;
  1976.  
  1977.         let wrapper = createFilterWrapper(filter);
  1978.         for each (let subscription in this.subscriptions)
  1979.         {
  1980.             // Replace filter by its wrapper in all subscriptions
  1981.             let index = -1;
  1982.             let found = false;
  1983.             do
  1984.             {
  1985.                 index = subscription.filters.indexOf(filter, index + 1);
  1986.                 if (index >= 0)
  1987.                 {
  1988.                     if (!subscription.hasOwnProperty("filters"))
  1989.                         subscription.filters = subscription.filters.slice();
  1990.  
  1991.                     subscription.filters[index] = wrapper;
  1992.                     found = true;
  1993.                 }
  1994.             } while (index >= 0);
  1995.  
  1996.             if (found)
  1997.             {
  1998.                 if (treeView.sortProc)
  1999.                 {
  2000.                     // Sorted filter list needs updating as well
  2001.                     index = -1;
  2002.                     do
  2003.                     {
  2004.                         index = subscription._sortedFilters.indexOf(filter, index + 1);
  2005.                         if (index >= 0)
  2006.                             subscription._sortedFilters[index] = wrapper;
  2007.                     } while (index >= 0);
  2008.                 }
  2009.                 else
  2010.                     subscription._sortedFilters = subscription.filters;
  2011.             }
  2012.         }
  2013.         return wrapper;
  2014.     },
  2015.  
  2016.     /**
  2017.      * Map of comparison functions by column ID  or column ID + "Desc" for
  2018.      * descending sort order.
  2019.      * @const
  2020.      */
  2021.     sortProcs:
  2022.     {
  2023.         filter: createSortFunction(compareText, null, false),
  2024.         filterDesc: createSortFunction(compareText, null, true),
  2025.         slow: createSortFunction(compareSlow, compareText, true),
  2026.         slowDesc: createSortFunction(compareSlow, compareText, false),
  2027.         enabled: createSortFunction(compareEnabled, compareText, false),
  2028.         enabledDesc: createSortFunction(compareEnabled, compareText, true),
  2029.         hitcount: createSortFunction(compareHitCount, compareText, false),
  2030.         hitcountDesc: createSortFunction(compareHitCount, compareText, true),
  2031.         lasthit: createSortFunction(compareLastHit, compareText, false),
  2032.         lasthitDesc: createSortFunction(compareLastHit, compareText, true)
  2033.     },
  2034.  
  2035.     /**
  2036.      * Changes sort direction of the list.
  2037.      * @param {Element} col column (<treecol>) the list should be sorted by
  2038.      * @param {String} direction either "natural" (unsorted), "ascending" or "descending"
  2039.      */
  2040.     resort: function(col, direction)
  2041.     {
  2042.         if (this.sortColumn)
  2043.             this.sortColumn.removeAttribute("sortDirection");
  2044.  
  2045.         if (direction == "natural")
  2046.         {
  2047.             this.sortColumn = null;
  2048.             this.sortProc = null;
  2049.         }
  2050.         else
  2051.         {
  2052.             this.sortColumn = col;
  2053.             this.sortProc = this.sortProcs[col.id.replace(/^col-/, "") + (direction == "descending" ? "Desc" : "")];
  2054.             this.sortColumn.setAttribute("sortDirection", direction);
  2055.         }
  2056.  
  2057.         for each (let subscription in this.subscriptions)
  2058.             this.resortSubscription(subscription);
  2059.  
  2060.         this.boxObject.invalidate();
  2061.     },
  2062.  
  2063.     /**
  2064.      * Updates subscription's _sortedFilters property (sorted index
  2065.      * of subscription's filters).
  2066.      */
  2067.     resortSubscription: function(/**Subscription*/ subscription)
  2068.     {
  2069.         if (this.sortProc)
  2070.         {
  2071.             // Hide comments in the list, they should be sorted like the filter following them
  2072.             let filters = subscription.filters.slice();
  2073.             let followingFilter = null;
  2074.             for (let i = filters.length - 1; i >= 0; i--)
  2075.             {
  2076.                 if (filters[i] instanceof abp.CommentFilter)
  2077.                     filters[i] = { __proto__: followingFilter, _origFilter: filters[i] };
  2078.                 else
  2079.                     followingFilter = filters[i];
  2080.             }
  2081.  
  2082.             filters.sort(this.sortProc);
  2083.  
  2084.             // Restore comments
  2085.             for (let i = 0; i < filters.length; i++)
  2086.                 if ("_origFilter" in filters[i])
  2087.                     filters[i] = filters[i]._origFilter;
  2088.  
  2089.             subscription._sortedFilters = filters;
  2090.         }
  2091.         else
  2092.             subscription._sortedFilters = subscription.filters;
  2093.     },
  2094.  
  2095.     /**
  2096.      * Selects given tree row.
  2097.      */
  2098.     selectRow: function(/**Integer*/ row)
  2099.     {
  2100.         treeView.selection.select(row);
  2101.         treeView.boxObject.ensureRowIsVisible(row);
  2102.     },
  2103.  
  2104.     /**
  2105.      * Finds the given filter in the list and selects it.
  2106.      */
  2107.     selectFilter: function(/**Filter*/ filter)
  2108.     {
  2109.         let resultSubscription = null;
  2110.         let resultIndex;
  2111.         for each (let subscription in this.subscriptions)
  2112.         {
  2113.             let index = subscription._sortedFilters.indexOf(filter);
  2114.             if (index >= 0)
  2115.             {
  2116.                 [resultSubscription, resultIndex] = [subscription, index];
  2117.  
  2118.                 // If the subscription is disabled continue searching - maybe
  2119.                 // we have the same filter in an enabled subscription as well
  2120.                 if (!subscription.disabled)
  2121.                     break;
  2122.             }
  2123.         }
  2124.  
  2125.         if (resultSubscription)
  2126.         {
  2127.             let parentRow = this.getSubscriptionRow(resultSubscription);
  2128.             if (resultSubscription.url in this.closed)
  2129.                 this.toggleOpenState(parentRow);
  2130.             this.selectRow(parentRow + 1 + resultSubscription._description.length + resultIndex);
  2131.         }
  2132.     },
  2133.  
  2134.     /**
  2135.      * This method will select the first row of a subscription.
  2136.      */
  2137.     selectSubscription: function(/**Subscription*/ subscription)
  2138.     {
  2139.         let row = this.getSubscriptionRow(subscription);
  2140.         if (row < 0)
  2141.             return;
  2142.  
  2143.         this.selection.select(row);
  2144.         this.boxObject.ensureRowIsVisible(row);
  2145.     },
  2146.  
  2147.     /**
  2148.      * This method will make sure that the list has some selection (assuming
  2149.      * that it has at least one entry).
  2150.      * @param {Integer} row   row to be selected if the list has no selection
  2151.      */
  2152.     ensureSelection: function(row)
  2153.     {
  2154.         if (this.selection.count == 0)
  2155.         {
  2156.             let rowCount = this.rowCount;
  2157.             if (row < 0)
  2158.                 row = 0;
  2159.             if (row >= rowCount)
  2160.                 row = rowCount - 1;
  2161.             if (row >= 0)
  2162.             {
  2163.                 this.selection.select(row);
  2164.                 this.boxObject.ensureRowIsVisible(row);
  2165.             }
  2166.         }
  2167.         else if (this.selection.currentIndex < 0)
  2168.         {
  2169.             let min = {};
  2170.             this.selection.getRangeAt(0, min, {});
  2171.             this.selection.currentIndex = min.value;
  2172.         }
  2173.     },
  2174.  
  2175.     /**
  2176.      * Checks whether there are any user-defined filters in the list.
  2177.      */
  2178.     hasUserFilters: function() /**Boolean*/
  2179.     {
  2180.         for each (let subscription in this.subscriptions)
  2181.             if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length)
  2182.                 return true;
  2183.  
  2184.         return false;
  2185.     },
  2186.  
  2187.     /**
  2188.      * Checks whether the given subscription is the first one displayed.
  2189.      */
  2190.     isFirstSubscription: function(/**Subscription*/ search) /**Boolean*/
  2191.     {
  2192.         for each (let subscription in this.subscriptions)
  2193.         {
  2194.             if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 0)
  2195.                 continue;
  2196.  
  2197.             return (subscription == search);
  2198.         }
  2199.         return false;
  2200.     },
  2201.  
  2202.     /**
  2203.      * Checks whether the given subscription is the last one displayed.
  2204.      */
  2205.     isLastSubscription: function(/**Subscription*/ search) /**Boolean*/
  2206.     {
  2207.         for (let i = this.subscriptions.length - 1; i >= 0; i--)
  2208.         {
  2209.             let subscription = this.subscriptions[i];
  2210.             if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 0)
  2211.                 continue;
  2212.  
  2213.             return (subscription == search);
  2214.         }
  2215.         return false;
  2216.     },
  2217.  
  2218.     /**
  2219.      * Adds a filter to a subscription. If no subscription is given, will
  2220.      * find one that accepts filters of this type.
  2221.      * @result {Subscription} the subscription this filter was added to
  2222.      */
  2223.     addFilter: function(/**Filter*/ filter, /**Subscription*/ subscription, /**Filter*/ insertBefore, /**Boolean*/ noSelect)
  2224.     {
  2225.         if (!filter)
  2226.             return null;
  2227.  
  2228.         if (!subscription)
  2229.         {
  2230.             for each (let s in this.subscriptions)
  2231.             {
  2232.                 if (s instanceof abp.SpecialSubscription && s.isFilterAllowed(filter))
  2233.                 {
  2234.                     if (s._sortedFilters.indexOf(filter) >= 0 || s.filters.indexOf(filter) >= 0)
  2235.                     {
  2236.                         subscription = s;
  2237.                         break;
  2238.                     }
  2239.  
  2240.                     if (!subscription || s.priority > subscription.priority)
  2241.                         subscription = s;
  2242.                 }
  2243.             }
  2244.         }
  2245.         if (!subscription)
  2246.             return null;
  2247.  
  2248.         let insertPositionSorted = subscription._sortedFilters.indexOf(filter);
  2249.         if (insertPositionSorted >= 0)
  2250.         {
  2251.             // We have that filter already, only need to select it
  2252.             if (!noSelect)
  2253.             {
  2254.                 let parentRow = this.getSubscriptionRow(subscription);
  2255.                 if (subscription.url in this.closed)
  2256.                     this.toggleOpenState(parentRow);
  2257.  
  2258.                 this.selectRow(parentRow + 1 + subscription._description.length + insertPositionSorted);
  2259.             }
  2260.             return subscription;
  2261.         }
  2262.  
  2263.         let insertPosition = -1;
  2264.         if (insertBefore)
  2265.             insertPosition = subscription.filters.indexOf(insertBefore);
  2266.         if (insertPosition < 0)
  2267.         {
  2268.             insertPosition = subscription.filters.length;
  2269.  
  2270.             // Insert before the comments at the end
  2271.             while (insertPosition > 0 && subscription.filters[insertPosition - 1] instanceof abp.CommentFilter && !(filter instanceof abp.CommentFilter))
  2272.                 insertPosition--;
  2273.             if (insertPosition == 0)
  2274.                 insertPosition = subscription.filters.length;
  2275.         }
  2276.  
  2277.         // If we don't have our own filters property the filter might be there already
  2278.         if (subscription.filters.indexOf(filter) < 0)
  2279.         {
  2280.             // Create a copy of the original subscription filters before modifying
  2281.             if (!subscription.hasOwnProperty("filters"))
  2282.                 subscription.filters = subscription.filters.slice();
  2283.  
  2284.             subscription.filters.splice(insertPosition, 0, filter);
  2285.         }
  2286.         this.resortSubscription(subscription);
  2287.         insertPositionSorted = subscription._sortedFilters.indexOf(filter);
  2288.  
  2289.         let parentRow = this.getSubscriptionRow(subscription);
  2290.  
  2291.         if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 1)
  2292.         {
  2293.             this.boxObject.rowCountChanged(parentRow, this.getSubscriptionRowCount(subscription));
  2294.         }
  2295.         else if (!(subscription.url in this.closed))
  2296.         {
  2297.             this.boxObject.rowCountChanged(parentRow + 1 + subscription._description.length + insertPositionSorted, 1);
  2298.             this.boxObject.invalidateRow(parentRow + 1 + subscription._description.length + insertPositionSorted);
  2299.         }
  2300.  
  2301.         if (!noSelect)
  2302.         {
  2303.             if (subscription.url in this.closed)
  2304.                 this.toggleOpenState(parentRow);
  2305.             this.selectRow(parentRow + 1 + subscription._description.length + insertPositionSorted);
  2306.         }
  2307.  
  2308.         onChange();
  2309.         return subscription;
  2310.     },
  2311.  
  2312.     /**
  2313.      * Adds a subscription to the list (if it isn't there already)
  2314.      * and makes sure it is selected.
  2315.      */
  2316.     addSubscription: function(/**Subscription*/ subscription, /**Boolean*/ noSelect)
  2317.     {
  2318.         if (this.subscriptions.indexOf(subscription) < 0)
  2319.         {
  2320.             this.subscriptions.push(subscription);
  2321.             this.boxObject.rowCountChanged(this.getSubscriptionRow(subscription), this.getSubscriptionRowCount(subscription));
  2322.         }
  2323.  
  2324.         if (!noSelect)
  2325.         {
  2326.             let [currentSelected, dummy] = this.getRowInfo(this.selection.currentIndex);
  2327.             if (currentSelected != subscription)
  2328.                 this.selectSubscription(subscription);
  2329.         }
  2330.     },
  2331.  
  2332.     /**
  2333.      * Removes a filter from the list.
  2334.      * @param {SpecialSubscription} subscription  the subscription the filter belongs to (if null, filter will be removed from all special subscriptions)
  2335.      * @param {Filter} filter filter to be removed
  2336.      */
  2337.     removeFilter: function(subscription, filter)
  2338.     {
  2339.         if (!subscription)
  2340.         {
  2341.             for each (let subscription in this.subscriptions)
  2342.             {
  2343.                 if (!(subscription instanceof abp.SpecialSubscription))
  2344.                     continue;
  2345.  
  2346.                 this.removeFilter(subscription, filter);
  2347.             }
  2348.             return;
  2349.         }
  2350.  
  2351.         let parentRow = this.getSubscriptionRow(subscription);
  2352.         let rowCount = this.getSubscriptionRowCount(subscription);
  2353.         let newSelection = parentRow;
  2354.  
  2355.         // The filter might be removed already if we don't have our own filters property yet
  2356.         let index = subscription.filters.indexOf(filter);
  2357.         if (index >= 0)
  2358.         {
  2359.             if (!subscription.hasOwnProperty("filters"))
  2360.                 subscription.filters = subscription.filters.slice();
  2361.  
  2362.             subscription.filters.splice(index, 1);
  2363.         }
  2364.  
  2365.         if (subscription.filters != subscription._sortedFilters)
  2366.             index = subscription._sortedFilters.indexOf(filter);
  2367.         if (index < 0)
  2368.             return;
  2369.  
  2370.         if (treeView.sortProc)
  2371.             subscription._sortedFilters.splice(index, 1);
  2372.         else
  2373.             subscription._sortedFilters = subscription.filters;
  2374.  
  2375.         if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length == 0)
  2376.         {
  2377.             // Empty special subscriptions aren't shown, remove everything
  2378.             this.boxObject.rowCountChanged(parentRow, -rowCount);
  2379.             newSelection -= rowCount;
  2380.         }
  2381.         else if (!(subscription.url in this.closed))
  2382.         {
  2383.             newSelection = parentRow + 1 + subscription._description.length + index;
  2384.             this.boxObject.rowCountChanged(newSelection, -1);
  2385.         }
  2386.  
  2387.         this.ensureSelection(newSelection);
  2388.         onChange();
  2389.     },
  2390.  
  2391.     /**
  2392.      * Removes a filter subscription from the list.
  2393.      * @param {RegularSubscription} subscription  filter subscription to be removed
  2394.      */
  2395.     removeSubscription: function(subscription)
  2396.     {
  2397.         let index = this.subscriptions.indexOf(subscription);
  2398.         if (index < 0)
  2399.             return;
  2400.  
  2401.         let firstRow = this.getSubscriptionRow(subscription);
  2402.         let rowCount = this.getSubscriptionRowCount(subscription);
  2403.  
  2404.         this.subscriptions.splice(index, 1);
  2405.         this.boxObject.rowCountChanged(firstRow, -rowCount);
  2406.  
  2407.         this.ensureSelection(firstRow);
  2408.         onChange();
  2409.     },
  2410.  
  2411.     /**
  2412.      * Moves a filter in the list up or down.
  2413.      * @param {Boolean} up  if true, the filter is moved up
  2414.      */
  2415.     moveFilter: function(up)
  2416.     {
  2417.         let oldRow = this.selection.currentIndex;
  2418.         let [subscription, filter] = this.getRowInfo(oldRow);
  2419.         if (this.isSorted() || !(filter instanceof abp.Filter) || !(subscription instanceof abp.SpecialSubscription))
  2420.             return;
  2421.  
  2422.         let oldIndex = subscription.filters.indexOf(filter);
  2423.         if (oldIndex < 0)
  2424.             return;
  2425.  
  2426.         let newIndex = (up ? oldIndex - 1 : oldIndex + 1);
  2427.         if (newIndex < 0 || newIndex >= subscription.filters.length)
  2428.             return;
  2429.  
  2430.         // Create a copy of the original subscription filters before modifying
  2431.         if (!subscription.hasOwnProperty("filters"))
  2432.         {
  2433.             subscription.filters = subscription.filters.slice();
  2434.             subscription._sortedFilters = subscription.filters;
  2435.         }
  2436.  
  2437.         [subscription.filters[oldIndex], subscription.filters[newIndex]] = [subscription.filters[newIndex], subscription.filters[oldIndex]];
  2438.  
  2439.         let newRow = oldRow - oldIndex + newIndex;
  2440.         this.boxObject.invalidateRange(Math.min(oldRow, newRow), Math.max(oldRow, newRow));
  2441.         this.selectRow(newRow);
  2442.  
  2443.         onChange();
  2444.     },
  2445.  
  2446.     /**
  2447.      * Moves a filter in the list up or down.
  2448.      * @param {Boolean} up  if true, the filter is moved up
  2449.      */
  2450.     moveSubscription: function(up)
  2451.     {
  2452.         let [subscription, filter] = this.getRowInfo(this.selection.currentIndex);
  2453.  
  2454.         let oldIndex = this.subscriptions.indexOf(subscription);
  2455.         if (oldIndex < 0)
  2456.             return;
  2457.  
  2458.         let oldRow = this.getSubscriptionRow(subscription);
  2459.         let offset = this.selection.currentIndex - oldRow;
  2460.         let newIndex = oldIndex;
  2461.         do
  2462.         {
  2463.             newIndex = (up ? newIndex - 1 : newIndex + 1);
  2464.             if (newIndex < 0 || newIndex >= this.subscriptions.length)
  2465.                 return;
  2466.         } while (this.subscriptions[newIndex] instanceof abp.SpecialSubscription && this.subscriptions[newIndex]._sortedFilters.length == 0);
  2467.  
  2468.         [this.subscriptions[oldIndex], this.subscriptions[newIndex]] = [this.subscriptions[newIndex], this.subscriptions[oldIndex]];
  2469.  
  2470.         let newRow = this.getSubscriptionRow(subscription);
  2471.         let rowCount = this.getSubscriptionRowCount(subscription);
  2472.         this.boxObject.invalidateRange(Math.min(oldRow, newRow), Math.max(oldRow, newRow) + rowCount - 1);
  2473.         this.selectRow(newRow + offset);
  2474.  
  2475.         onChange();
  2476.     },
  2477.  
  2478.     dragSubscription: null,
  2479.     dragFilter: null,
  2480.     startDrag: function(row)
  2481.     {
  2482.         let [subscription, filter] = this.getRowInfo(row);
  2483.         if (!subscription)
  2484.             return;
  2485.         if (filter instanceof abp.Filter && !(subscription instanceof abp.SpecialSubscription))
  2486.             return;
  2487.         if (filter instanceof abp.Filter && !(filter instanceof abp.CommentFilter) && this.isSorted())
  2488.             return;
  2489.  
  2490.         if (!(filter instanceof abp.Filter))
  2491.             filter = null;
  2492.  
  2493.         let array = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
  2494.         let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
  2495.         let data = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
  2496.         if (filter instanceof abp.Filter)
  2497.             data.data = filter.text;
  2498.         else
  2499.             data.data = subscription.title;
  2500.         transferable.setTransferData("text/unicode", data, data.data.length * 2);
  2501.         array.AppendElement(transferable);
  2502.  
  2503.         let region = Cc["@mozilla.org/gfx/region;1"].createInstance(Ci.nsIScriptableRegion);
  2504.         region.init();
  2505.         let x = {};
  2506.         let y = {};
  2507.         let width = {};
  2508.         let height = {};
  2509.         let col = this.boxObject.columns.getPrimaryColumn();
  2510.         this.boxObject.getCoordsForCellItem(row, col, "text", x, y, width, height);
  2511.         region.setToRect(x.value, y.value, width.value, height.value);
  2512.  
  2513.         this.dragSubscription = subscription;
  2514.         this.dragFilter = filter;
  2515.  
  2516.         // This will throw an exception if the user cancels D&D
  2517.         try {
  2518.             dragService.invokeDragSession(this.boxObject.treeBody, array, region, dragService.DRAGDROP_ACTION_MOVE);
  2519.         } catch(e) {}
  2520.     },
  2521.  
  2522.     /**
  2523.      * Toggles disabled state of the selected filters/subscriptions.
  2524.      * @param {Array of Filter or Subscription} items
  2525.      */
  2526.     toggleDisabled: function(items)
  2527.     {
  2528.         let newValue;
  2529.         for each (let item in items)
  2530.         {
  2531.             if (!(item instanceof abp.ActiveFilter || item instanceof abp.Subscription))
  2532.                 return;
  2533.  
  2534.             if (item instanceof abp.ActiveFilter)
  2535.                 item = this.ensureFilterWrapper(item);
  2536.  
  2537.             if (typeof newValue == "undefined")
  2538.                 newValue = !item.disabled;
  2539.  
  2540.             if (!newValue)
  2541.             {
  2542.                 if (item instanceof abp.Subscription)
  2543.                 {
  2544.                     for each (let filter in item._sortedFilters)
  2545.                         ensureFilterShortcut(filter);
  2546.                 }
  2547.                 else
  2548.                     ensureFilterShortcut(item);
  2549.             }
  2550.  
  2551.             item.disabled = newValue;
  2552.         }
  2553.  
  2554.         if (typeof newValue != "undefined")
  2555.         {
  2556.             this.boxObject.invalidate();
  2557.             onChange();
  2558.         }
  2559.     },
  2560.  
  2561.     /**
  2562.      * Invalidates all instances of a filter in the list, making sure changes
  2563.      * are displayed.
  2564.      */
  2565.     invalidateFilter: function(/**Filter*/ search)
  2566.     {
  2567.         let min = this.boxObject.getFirstVisibleRow();
  2568.         let max = this.boxObject.getLastVisibleRow();
  2569.         for (let i = min; i <= max; i++)
  2570.         {
  2571.             let [subscription, filter] = this.getRowInfo(i);
  2572.             if (filter == filter)
  2573.                 this.boxObject.invalidateRow(i);
  2574.         }
  2575.     },
  2576.  
  2577.     /**
  2578.      * Invalidates a subscription in the list, making sure changes are displayed.
  2579.      * @param {Subscription} subscription
  2580.      * @param {Integer} oldRowCount  (optional) number of roww in the subscription before the change
  2581.      */
  2582.     invalidateSubscription: function(subscription, oldRowCount)
  2583.     {
  2584.         let row = this.getSubscriptionRow(subscription);
  2585.         if (row < 0)
  2586.             return;
  2587.  
  2588.         let rowCount = this.getSubscriptionRowCount(subscription);
  2589.         if (typeof oldRowCount != "undefined" && rowCount != oldRowCount)
  2590.             this.boxObject.rowCountChanged(row + Math.min(rowCount, oldRowCount), rowCount - oldRowCount);
  2591.  
  2592.         if (typeof oldRowCount != "undefined" && oldRowCount < rowCount)
  2593.             rowCount = oldRowCount;
  2594.         this.boxObject.invalidateRange(row, row + rowCount - 1);
  2595.     },
  2596.  
  2597.     /**
  2598.      * Makes sure the description rows of the subscription are updated.
  2599.      */
  2600.     invalidateSubscriptionInfo: function(/**Subscription*/subscription)
  2601.     {
  2602.         let row = this.getSubscriptionRow(subscription);
  2603.  
  2604.         let oldCount = subscription._description.length;
  2605.         subscription._description = getSubscriptionDescription(subscription);
  2606.         let newCount = subscription._description.length;
  2607.         if (oldCount != newCount)
  2608.             this.boxObject.rowCountChanged(row + Math.min(oldCount, newCount), newCount - oldCount);
  2609.  
  2610.         this.boxObject.invalidateRange(row, row + newCount);
  2611.     },
  2612.  
  2613.     /**
  2614.      * Removes all user-defined filters from the list.
  2615.      */
  2616.     removeUserFilters: function()
  2617.     {
  2618.         for each (let subscription in this.subscriptions)
  2619.         {
  2620.             if (subscription instanceof abp.SpecialSubscription && subscription._sortedFilters.length > 0)
  2621.             {
  2622.                 let row = this.getSubscriptionRow(subscription);
  2623.                 let count = this.getSubscriptionRowCount(subscription);
  2624.  
  2625.                 subscription.filters = [];
  2626.                 subscription._sortedFilters = subscription.filters;
  2627.                 this.boxObject.rowCountChanged(row, -count);
  2628.  
  2629.                 onChange();
  2630.             }
  2631.         }
  2632.         this.ensureSelection(0);
  2633.     },
  2634.  
  2635.     /**
  2636.      * Saves all changes back to filter storage.
  2637.      */
  2638.     applyChanges: function()
  2639.     {
  2640.         try
  2641.         {
  2642.             abp.filterListener.batchMode = true;
  2643.  
  2644.             let oldSubscriptions = {__proto__: null};
  2645.             for each (let subscription in filterStorage.subscriptions)
  2646.                 oldSubscriptions[subscription.url] = true;
  2647.  
  2648.             let newSubscriptions = {__proto__: null};
  2649.             let subscriptions = [];
  2650.             for each (let subscription in this.subscriptions)
  2651.             {
  2652.                 let changed = false;
  2653.                 let disableChanged = (subscription.disabled != subscription.__proto__.disabled);
  2654.                 for (let key in subscription)
  2655.                 {
  2656.                     if (subscription.hasOwnProperty(key) && key[0] != "_" && key != "filters")
  2657.                     {
  2658.                         subscription.__proto__[key] = subscription[key];
  2659.                         delete subscription[key];
  2660.                         changed = true;
  2661.                     }
  2662.                 }
  2663.  
  2664.                 let hasFilters = {__proto__: null};
  2665.                 let hadWrappers = false;
  2666.                 for (let i = 0; i < subscription.filters.length; i++)
  2667.                 {
  2668.                     let filter = subscription.filters[i];
  2669.                     if ("_isWrapper" in filter)
  2670.                     {
  2671.                         if (filter.disabled != filter.__proto__.disabled)
  2672.                         {
  2673.                             filter.__proto__.disabled = filter.disabled;
  2674.                             filterStorage.triggerFilterObservers(filter.disabled ? "disable" : "enable", [filter.__proto__]);
  2675.                         }
  2676.                         subscription.filters[i] = filter.__proto__;
  2677.                         hadWrappers = true;
  2678.                     }
  2679.                     hasFilters[filter.text] = true;
  2680.                 }
  2681.  
  2682.                 let filtersChanged = (subscription.filters.length != subscription.__proto__.filters.length);
  2683.                 if (!filtersChanged)
  2684.                 {
  2685.                     for each (let filter in subscription.__proto__.filters)
  2686.                     {
  2687.                         if (!(filter.text in hasFilters))
  2688.                         {
  2689.                             filtersChanged = true;
  2690.                             break;
  2691.                         }
  2692.                     }
  2693.                 }
  2694.  
  2695.                 if (!(subscription.url in oldSubscriptions))
  2696.                     filterStorage.addSubscription(subscription.__proto__);
  2697.                 else if (filtersChanged)
  2698.                     filterStorage.updateSubscriptionFilters(subscription.__proto__, subscription.filters);
  2699.                 else if (changed)
  2700.                 {
  2701.                     filterStorage.triggerSubscriptionObservers("updateinfo", [subscription.__proto__]);
  2702.                     if (disableChanged)
  2703.                         filterStorage.triggerSubscriptionObservers(subscription.disabled ? "disable" : "enable", [subscription.__proto__]);
  2704.                 }
  2705.  
  2706.                 // Even if the filters didn't change, their ordering might have
  2707.                 // changed. Replace filters on the original subscription without
  2708.                 // triggering observers.
  2709.                 subscription.__proto__.filters = subscription.filters;
  2710.                 delete subscription.filters;
  2711.  
  2712.                 if (hadWrappers)
  2713.                 {
  2714.                     // Reinitialize _sortedFilters to remove wrappers from it
  2715.                     this.resortSubscription(subscription);
  2716.                 }
  2717.  
  2718.                 newSubscriptions[subscription.url] = true;
  2719.                 subscriptions.push(subscription.__proto__);
  2720.             }
  2721.  
  2722.             filterWrappers = {__proto__: null};
  2723.  
  2724.             for each (let subscription in filterStorage.subscriptions.slice())
  2725.                 if (!(subscription.url in newSubscriptions))
  2726.                     filterStorage.removeSubscription(subscription);
  2727.  
  2728.             // Make sure that filter storage has the subscriptions in correct order,
  2729.             // replace subscriptions list without triggering observers.
  2730.             filterStorage.subscriptions = subscriptions;
  2731.  
  2732.             filterStorage.saveToDisk();
  2733.         }
  2734.         finally
  2735.         {
  2736.             abp.filterListener.batchMode = false;
  2737.         }
  2738.     },
  2739.  
  2740.     /**
  2741.      * Searches a text string in the subscription titles, subscription
  2742.      * descriptions and filters. Selects the matches.
  2743.      * @param {String} text  text being searched
  2744.      * @param {Integer} direction 1 for searching forwards from current position,
  2745.      *                            -1 for searching backwards,
  2746.      *                            0 for searching forwards but including current position as well
  2747.      * @param {Boolean} highlightAll if true, all matches will be selected and not only the current one
  2748.      * @param {Boolean} caseSensitive if true, string comparisons should be case-sensitive
  2749.      * @return {Integer} one of the nsITypeAheadFind constants
  2750.      */
  2751.     find: function(text, direction, highlightAll, caseSensitive)
  2752.     {
  2753.         function normalizeString(string)
  2754.         {
  2755.             return caseSensitive ? string : string.toLowerCase();
  2756.         }
  2757.         text = normalizeString(text);
  2758.  
  2759.         // Matches: current row, first match, previous match, next match, last match
  2760.         let match = [null, null, null, null, null];
  2761.         let [currentSubscription, currentFilter] = this.getRowInfo(this.selection.currentIndex);
  2762.         let isCurrent = false;
  2763.         let foundCurrent = !currentSubscription;
  2764.         let rowCache = {__proto__: null};
  2765.         if (highlightAll)
  2766.             this.selection.clearSelection();
  2767.  
  2768.         let selectMatch = function(subscription, offset)
  2769.         {
  2770.             if (highlightAll)
  2771.             {
  2772.                 if (!(subscription.url in rowCache))
  2773.                     rowCache[subscription.url] = treeView.getSubscriptionRow(subscription);
  2774.  
  2775.                 let row = rowCache[subscription.url];
  2776.                 if (offset && subscription.url in treeView.closed)
  2777.                     treeView.toggleOpenState(row);
  2778.                 treeView.selection.rangedSelect(row + offset, row + offset, true);
  2779.             }
  2780.  
  2781.             let index = (isCurrent ? 0 : (foundCurrent ?  4 : 2));
  2782.             match[index] = [subscription, offset];
  2783.             if (index > 0 && !match[index - 1])
  2784.                 match[index - 1] = match[index];
  2785.         };
  2786.  
  2787.         for each (let subscription in this.subscriptions)
  2788.         {
  2789.             // Skip invisible subscriptions
  2790.             let rowCount = this.getSubscriptionRowCount(subscription);
  2791.             if (rowCount == 0)
  2792.                 continue;
  2793.  
  2794.             let offset = 0;
  2795.             isCurrent = (subscription == currentSubscription && !currentFilter);
  2796.             if (normalizeString(subscription.title).indexOf(text) >= 0)
  2797.                 selectMatch(subscription, offset);
  2798.             if (isCurrent)
  2799.                 foundCurrent = true;
  2800.             offset++;
  2801.  
  2802.             for each (let description in subscription._description)
  2803.             {
  2804.                 isCurrent = (subscription == currentSubscription && currentFilter === description);
  2805.                 if (normalizeString(description).indexOf(text) >= 0)
  2806.                     selectMatch(subscription, offset);
  2807.                 if (isCurrent)
  2808.                     foundCurrent = true;
  2809.                 offset++;
  2810.             }
  2811.  
  2812.             for each (let filter in subscription._sortedFilters)
  2813.             {
  2814.                 isCurrent = (subscription == currentSubscription && filter == currentFilter);
  2815.                 if (normalizeString(filter.text).indexOf(text) >= 0)
  2816.                     selectMatch(subscription, offset);
  2817.                 if (isCurrent)
  2818.                     foundCurrent = true;
  2819.                 offset++;
  2820.             }
  2821.         }
  2822.  
  2823.         let found = null;
  2824.         let status = "";
  2825.         if (direction == 0)
  2826.             found = match[0] || match[3] || match[1];
  2827.         else if (direction > 0)
  2828.             found = match[3] || match[1] || match[0];
  2829.         else
  2830.             found = match[2] || match[4] || match[0];
  2831.  
  2832.         if (!found)
  2833.             return Ci.nsITypeAheadFind.FIND_NOTFOUND;
  2834.  
  2835.         let [subscription, offset] = found;
  2836.         let row = this.getSubscriptionRow(subscription);
  2837.         if (offset && subscription.url in this.closed)
  2838.             this.toggleOpenState(row);
  2839.         if (highlightAll)
  2840.             this.selection.currentIndex = row + offset;
  2841.         else
  2842.             this.selection.select(row + offset);
  2843.         this.boxObject.ensureRowIsVisible(row + offset);
  2844.  
  2845.         if (direction < 0 && found != match[2])
  2846.             return Ci.nsITypeAheadFind.FIND_WRAPPED;
  2847.         if ((direction > 0 && found != match[3]) || (direction == 0 && found == match[1]))
  2848.             return Ci.nsITypeAheadFind.FIND_WRAPPED;
  2849.  
  2850.         return Ci.nsITypeAheadFind.FIND_FOUND;
  2851.     },
  2852.  
  2853.     //
  2854.     // Inline filter editor
  2855.     //
  2856.  
  2857.     editor: null,
  2858.     editorParent: null,
  2859.     editedRow: -1,
  2860.     editorKeyPressHandler: null,
  2861.     editorBlurHandler: null,
  2862.     editorCancelHandler: null,
  2863.     editorDummy: null,
  2864.     editorDummyInit: "",
  2865.  
  2866.     /**
  2867.      * true if the editor is currently open
  2868.      * @type Boolean
  2869.      */
  2870.     get isEditing()
  2871.     {
  2872.         return (this.editedRow >= 0);
  2873.     },
  2874.  
  2875.     /**
  2876.      * Initializes inline editor.
  2877.      * @param {Element} editor  text field to be used as inline editor
  2878.      * @param {Element} editorParent  editor's parent node to be made visible when the editor should be shown
  2879.      */
  2880.     setEditor: function(editor, editorParent)
  2881.     {
  2882.         this.editor = editor;
  2883.         this.editorParent = editorParent;
  2884.  
  2885.         let me = this;
  2886.         this.editorKeyPressHandler = function(e)
  2887.         {
  2888.             if (e.keyCode == e.DOM_VK_RETURN || e.keyCode == e.DOM_VK_ENTER)
  2889.             {
  2890.                 me.stopEditor(true);
  2891.                 if (e.ctrlKey || e.altKey || e.metaKey)
  2892.                     document.documentElement.acceptDialog();
  2893.                 else
  2894.                 {
  2895.                     e.preventDefault();
  2896.                     e.stopPropagation();
  2897.                 }
  2898.             }
  2899.             else if (e.keyCode == e.DOM_VK_CANCEL || e.keyCode == e.DOM_VK_ESCAPE)
  2900.             {
  2901.                 me.stopEditor(false);
  2902.                 e.preventDefault();
  2903.                 e.stopPropagation();
  2904.             }
  2905.         };
  2906.         this.editorBlurHandler = function(e)
  2907.         {
  2908.             setTimeout(function()
  2909.             {
  2910.                 let focused = document.commandDispatcher.focusedElement;
  2911.                 if (!focused || focused != me.editor.field)
  2912.                     me.stopEditor(true, true);
  2913.             }, 0);
  2914.         };
  2915.  
  2916.         // Prevent cyclic references through closures
  2917.         editor = null;
  2918.         editorParent = null;
  2919.     },
  2920.  
  2921.     /**
  2922.      * Opens inline editor.
  2923.      * @param {Boolean} insert  if false, the editor will insert a new filter, otherwise edit currently selected filter
  2924.      */
  2925.     startEditor: function(insert)
  2926.     {
  2927.         this.stopEditor(false);
  2928.  
  2929.         let row = this.selection.currentIndex;
  2930.         let [subscription, filter] = this.getRowInfo(row);
  2931.         if (!(subscription instanceof abp.SpecialSubscription) || !(filter instanceof abp.Filter))
  2932.         {
  2933.             let dummySubscription = new abp.Subscription("~dummy~");
  2934.             dummySubscription.title = abp.getString("new_filter_group_title");
  2935.             dummySubscription.filters.push(" ");
  2936.             dummySubscription = createSubscriptionWrapper(dummySubscription);
  2937.  
  2938.             this.subscriptions.unshift(dummySubscription);
  2939.             this.boxObject.rowCountChanged(0, this.getSubscriptionRowCount(dummySubscription));
  2940.  
  2941.             row = 1;
  2942.             this.selectRow(row);
  2943.             this.editorDummy = dummySubscription;
  2944.         }
  2945.         else if (insert)
  2946.         {
  2947.             if (subscription._sortedFilters == subscription.filters)
  2948.                 subscription._sortedFilters = subscription.filters.slice();
  2949.  
  2950.             let index = subscription._sortedFilters.indexOf(filter);
  2951.             subscription._sortedFilters.splice(index, 0, " ");
  2952.             this.boxObject.rowCountChanged(row, 1);
  2953.  
  2954.             this.selectRow(row);
  2955.             this.editorDummy = [subscription, index];
  2956.         }
  2957.  
  2958.         let col = this.boxObject.columns.getPrimaryColumn();
  2959.         let textX = {};
  2960.         let textY = {};
  2961.         let textWidth = {};
  2962.         let textHeight = {};
  2963.         this.boxObject.ensureRowIsVisible(row);
  2964.         this.boxObject.getCoordsForCellItem(row, col, "text", textX, textY, textWidth, textHeight);
  2965.  
  2966.         let cellX = {};
  2967.         let cellWidth = {};
  2968.         this.boxObject.getCoordsForCellItem(row, col, "cell", cellX, {}, cellWidth, {});
  2969.         cellWidth.value -= textX.value - cellX.value;
  2970.  
  2971.         // Need to translate coordinates so that they are relative to <stack>, not <treechildren>
  2972.         let treeBody = this.boxObject.treeBody;
  2973.         let editorStack = this.editorParent.parentNode;
  2974.         textX.value += treeBody.boxObject.x - editorStack.boxObject.x;
  2975.         textY.value += treeBody.boxObject.y - editorStack.boxObject.y;
  2976.  
  2977.         this.selection.clearSelection();
  2978.  
  2979.         let style = window.getComputedStyle(this.editor, "");
  2980.         let topadj = parseInt(style.borderTopWidth) + parseInt(style.paddingTop);
  2981.  
  2982.         this.editedRow = row;
  2983.         this.editorParent.hidden = false;
  2984.         this.editorParent.width = cellWidth.value;
  2985.         this.editorParent.height = textHeight.value + topadj + parseInt(style.borderBottomWidth) + parseInt(style.paddingBottom);
  2986.         this.editorParent.left = textX.value;
  2987.         this.editorParent.top = textY.value - topadj;
  2988.  
  2989.         let text = (this.editorDummy ? this.editorDummyInit : filter.text);
  2990.  
  2991.         this.editor.focus();
  2992.         this.editor.field = document.commandDispatcher.focusedElement;
  2993.         this.editor.field.value = text;
  2994.         this.editor.field.setSelectionRange(this.editor.value.length, this.editor.value.length);
  2995.  
  2996.         // Need to attach handlers to the embedded html:input instead of menulist - won't catch blur otherwise
  2997.         this.editor.field.addEventListener("keypress", this.editorKeyPressHandler, false);
  2998.         this.editor.field.addEventListener("blur", this.editorBlurHandler, false);
  2999.  
  3000.         this.boxObject.invalidateRow(row);
  3001.     },
  3002.  
  3003.     /**
  3004.      * Closes inline editor.
  3005.      * @param {Boolean} save  if true, the editor result should be saved (user accepted changes)
  3006.      * @param {Boolean} blur  if true, editor was closed on blur and the list shouldn't be focused
  3007.      */
  3008.     stopEditor: function(save, blur)
  3009.     {
  3010.         if (this.editedRow < 0)
  3011.             return;
  3012.  
  3013.         this.editor.field.removeEventListener("keypress", this.editorKeyPressHandler, false);
  3014.         this.editor.field.removeEventListener("blur", this.editorBlurHandler, false);
  3015.  
  3016.         let insert = (this.editorDummy != null);
  3017.         if (this.editorDummy instanceof abp.Subscription)
  3018.         {
  3019.             let rowCount = this.getSubscriptionRowCount(this.editorDummy);
  3020.             this.subscriptions.shift();
  3021.             this.boxObject.rowCountChanged(0, -rowCount);
  3022.             this.selectRow(0);
  3023.             this.editedRow = -1;
  3024.         }
  3025.         else if (this.editorDummy)
  3026.         {
  3027.             let [subscription, index] = this.editorDummy;
  3028.             subscription._sortedFilters.splice(index, 1);
  3029.             this.boxObject.rowCountChanged(this.editedRow, -1);
  3030.             this.selectRow(this.editedRow);
  3031.         }
  3032.         else
  3033.             this.selectRow(this.editedRow);
  3034.  
  3035.         if (typeof blur == "undefined" || !blur)
  3036.             this.boxObject.treeBody.parentNode.focus();
  3037.  
  3038.         let [subscription, filter] = this.getRowInfo(this.editedRow);
  3039.         let text = abp.normalizeFilter(this.editor.value);
  3040.         if (save && text && (insert || !(filter instanceof abp.Filter) || text != filter.text))
  3041.         {
  3042.             let newFilter = getFilterByText(text);
  3043.             if (filter && subscription.isFilterAllowed(newFilter))
  3044.                 this.addFilter(newFilter, subscription, filter);
  3045.             else
  3046.                 this.addFilter(newFilter);
  3047.  
  3048.             if (!insert)
  3049.                 this.removeFilter(subscription, filter);
  3050.  
  3051.             onChange();
  3052.         }
  3053.  
  3054.         this.editor.field.value = "";
  3055.         this.editorParent.hidden = true;
  3056.  
  3057.         this.editedRow = -1;
  3058.         this.editorDummy = null;
  3059.         this.editorDummyInit = (save ? "" : text);
  3060.     }
  3061. };
  3062.